code
- # - ### Header → Header
- # - Escape HTML special chars first
- # - Remove > blockquote prefix
- CHANGELOG=$(echo "$FULL_CHANGELOG" | \
- sed 's/^> //' | \
+ # Convert Markdown to Telegram HTML
+ CHANGELOG=$(cat /tmp/cliff_tg.txt | \
+ sed '/^\*\*Full Changelog\*\*/d' | \
+ sed 's/\[#\([0-9]*\)\]([^)]*)/#\1/g' | \
+ sed 's/\[@\([^]]*\)\]([^)]*)/@\1/g' | \
sed 's/&/\&/g' | \
sed 's/\</g' | \
sed 's/>/\>/g' | \
- sed 's/`\([^`]*\)`/\1<\/code>/g' | \
sed 's/\*\*\([^*]*\)\*\*/\1<\/b>/g' | \
sed 's/^### \(.*\)$/\1<\/b>/g' | \
sed 's/^## \(.*\)$/\1<\/b>/g' | \
- sed 's/^- /• /g' | \
- sed 's/^ - / ◦ /g')
-
- # Take first 2500 characters, then cut at last complete line
+ sed 's/^- /• /g')
+
+ # Truncate for Telegram 4096 char limit
CHANGELOG=$(echo "$CHANGELOG" | head -c 2500 | sed '$d')
-
- # Check if truncated
- FULL_LEN=${#FULL_CHANGELOG}
- if [ $FULL_LEN -gt 2500 ]; then
- CHANGELOG="${CHANGELOG}"$'\n\n... (see full changelog on GitHub)'
- fi
+ echo "$CHANGELOG" > /tmp/changelog.txt
fi
- echo "$CHANGELOG" > /tmp/changelog.txt
- echo "DEBUG: Final changelog:"
+ echo "Telegram changelog:"
cat /tmp/changelog.txt
- name: Send to Telegram Channel
diff --git a/.gitignore b/.gitignore
index e2e2649f..3c57112b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,6 +12,9 @@ Thumbs.db
# Kiro specs (development only)
.kiro/
+# Design assets (banners, mockups)
+design/
+
# Reference folder (development only)
referensi/
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 25d41bf0..ec8b7254 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,54 @@
# Changelog
+## [3.7.2] - 2026-03-07
+
+### Changed
+
+- **Amazon Music is now an Extension**: Amazon Music has been moved from a built-in service to a separate installable extension. Install the "Amazon Music" extension from the Store to continue using it.
+
+### Fixed
+
+- **Deezer Downloads Timing Out**: Deezer downloads were failing with "context deadline exceeded" on larger files. Now uses the proper download timeout, matching Tidal and Qobuz.
+- **iOS Local Library Scan Fails**: Local library scanning was failing on iOS because the app lost access to user-picked folders after the FilePicker session ended. Implemented iOS security-scoped bookmark system:
+ - When a library folder is picked on iOS, a security-scoped bookmark is created and persisted in settings (`localLibraryBookmark`)
+ - Before each scan, the bookmark is resolved and security-scoped access is started; access is released in `finally` block after scan completes
+ - `cleanupMissingFiles` also activates the bookmark before checking file existence on iOS
+ - New `AppDelegate.swift` method channel handlers: `createIosBookmarkFromPath`, `startAccessingIosBookmark`, `stopAccessingIosBookmark`, `resolveIosBookmark`
+ - New `PlatformBridge` methods: `createIosBookmarkFromPath()`, `startAccessingIosBookmark()`, `stopAccessingIosBookmark()`
+ - All scan call-sites (Library Settings, Queue tab, Local Album screen) now pass the iOS bookmark to `startScan()`
+
+### Added
+
+- **Amazon Music Extension**: Available in `extension/Amazon-SpotiFLAC/` — same functionality as before, now as an installable extension.
+- **Accessibility Tooltips**: Added localized tooltips to all `IconButton` and `PopupMenuButton` widgets across the entire UI for screen reader and long-press discoverability
+ - Back buttons use `MaterialLocalizations.backButtonTooltip`
+ - Close buttons use `MaterialLocalizations.closeButtonTooltip`
+ - Menu buttons use `MaterialLocalizations.showMenuTooltip`
+ - Search buttons use `MaterialLocalizations.searchFieldLabel`
+ - Contextual actions use descriptive labels: "Play track", "Dismiss", "Clear search", "Change folder", "Refresh"
+ - Screens affected: Album, Artist, Playlist, Downloaded Album, Local Album, Home, Search, Queue, Library Playlists, Library Tracks Folder, Setup, Tutorial, Track Metadata, Store, Extension Store Details, and all Settings sub-pages (About, Appearance, Cache Management, Donate, Download, Extensions, Extension Detail, Library, Log, Options, Provider Priority)
+- **Semantics Wrappers**: Added `Semantics` widgets to interactive elements that previously had no accessibility information
+ - Album tiles in Artist screen: announces selection state and album name
+ - Recently downloaded track tiles in Home tab: announces track name and artist
+ - Explore items (albums/artists/playlists) in Home tab: announces item type and name
+ - Color palette picker in Appearance settings: announces selected state and color hex value
+ - Download button demo in Tutorial screen: added `ExcludeSemantics` on icon to prevent duplicate screen reader announcements
+ - Queue tab playlist cards: announces playlist name and item count
+ - Queue tab downloaded album cards: announces album name, artist, and track count
+ - Queue tab local album cards: announces album name, artist, and track count
+ - Queue tab play button on completed downloads: announces track name and artist with `ExcludeSemantics` on icon
+ - Queue tab download status indicators: "Finalizing download", "Download completed", "Downloaded file missing" labels with `ExcludeSemantics` on icons
+
+### Improved
+
+- **Code Formatting**: Reformatted and corrected indentation across multiple files to comply with Dart style guidelines
+ - `extension_detail_page.dart`: Fixed `SliverAppBar` and all subsequent slivers indentation (was 2 spaces short)
+ - `log_screen.dart`: Fixed `SliverAppBar` indentation alignment
+ - `donate_page.dart`: Reformatted ternary expressions and `_cr` function body
+ - `library_tracks_folder_screen.dart`: Minor line-break formatting
+
+---
+
## [3.7.1] - 2026-03-06
### Added
diff --git a/README.md b/README.md
index 83669b33..c99cb71b 100644
--- a/README.md
+++ b/README.md
@@ -1,20 +1,13 @@
-[](https://github.com/zarzet/SpotiFLAC-Mobile/releases)
-[](https://www.virustotal.com/gui/file/0a2bd2a033551983fc9fcd83f82fd912c83914fd1094cd8d1c7c6a68eb23233f)
-[](https://crowdin.com/project/spotiflac-mobile)
-
-
-
-Download music in true lossless FLAC from Tidal, Qobuz & Amazon Music — no account required.
-
-
-
+
+
+
+
+
-### [Download](https://github.com/zarzet/SpotiFLAC-Mobile/releases)
-
## Screenshots
@@ -24,6 +17,17 @@ Download music in true lossless FLAC from Tidal, Qobuz & Amazon Music — no acc
+
+
+[](https://github.com/zarzet/SpotiFLAC-Mobile/releases)
+[](https://www.virustotal.com/gui/file/0a2bd2a033551983fc9fcd83f82fd912c83914fd1094cd8d1c7c6a68eb23233f)
+[](https://crowdin.com/project/spotiflac-mobile)
+
+[](https://t.me/spotiflac)
+[](https://t.me/spotiflac_chat)
+
+
+
## Extensions
Extensions allow the community to add new music sources and features without waiting for app updates. When a streaming service API changes or a new source becomes available, extensions can be updated independently.
@@ -43,18 +47,13 @@ Want to create your own extension? Check out the [Extension Development Guide](h
### [SpotiFLAC (Desktop)](https://github.com/afkarxyz/SpotiFLAC)
Download music in true lossless FLAC from Tidal, Qobuz & Amazon Music for Windows, macOS & Linux
-## Telegram
-
-[](https://t.me/spotiflac)
-[](https://t.me/spotiflac_chat)
-
## FAQ
**Q: Why is my download failing with "Song not found"?**
-A: The track may not be available on Tidal, Qobuz, or Amazon Music. Try enabling more download services in Settings > Download > Provider Priority, or install additional extensions from the Store.
+A: The track may not be available on the streaming services. Try enabling more download services in Settings > Download > Provider Priority, or install additional extensions like Amazon Music from the Store.
**Q: Why are some tracks downloading in lower quality?**
-A: Quality depends on what's available from the streaming service. Tidal offers up to 24-bit/192kHz, Qobuz up to 24-bit/192kHz, and Amazon up to 24-bit/48kHz. The app automatically selects the best available quality.
+A: Quality depends on what's available from the streaming service and extensions. Built-in providers: Tidal offers up to 24-bit/192kHz, Qobuz up to 24-bit/192kHz, and Deezer up to 16-bit/44.1kHz.
**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.
@@ -75,23 +74,6 @@ _If this software is useful and brings you value, consider supporting the projec
[](https://ko-fi.com/zarzet)
-
-## Disclaimer
-
-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 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.
-2. Reading and adhering to the Terms of Service of the respective platforms.
-3. Any legal consequences resulting from the misuse of this tool.
-
-The software is provided "as is", without warranty of any kind. The author assumes no liability for any bans, damages, or legal issues arising from its use.
-
-
## API Credits
[hifi-api](https://github.com/binimum/hifi-api) · [music.binimum.org](https://music.binimum.org) · [qqdl.site](https://qqdl.site) · [squid.wtf](https://squid.wtf) · [spotisaver.net](https://spotisaver.net) · [dabmusic.xyz](https://dabmusic.xyz) · [AfkarXYZ](https://github.com/afkarxyz) · [LRCLib](https://lrclib.net) · [Paxsenix](https://lyrics.paxsenix.org) · [Cobalt](https://cobalt.tools) · [qwkuns.me](https://qwkuns.me) · [SpotubeDL](https://spotubedl.com) · [Song.link](https://song.link) · [IDHS](https://github.com/sjdonado/idonthavespotify)
diff --git a/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt b/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt
index c30a1775..5d5650da 100644
--- a/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt
+++ b/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt
@@ -766,6 +766,27 @@ class MainActivity: FlutterFragmentActivity() {
val response = downloader(req.toString())
val respObj = JSONObject(response)
if (respObj.optBoolean("success", false)) {
+ // Extension providers write to a local temp path instead of the SAF FD.
+ // Copy the local file into the SAF document so it is not empty.
+ val goFilePath = respObj.optString("file_path", "")
+ if (goFilePath.isNotEmpty() &&
+ !goFilePath.startsWith("content://") &&
+ !goFilePath.startsWith("/proc/self/fd/")
+ ) {
+ try {
+ val srcFile = java.io.File(goFilePath)
+ if (srcFile.exists() && srcFile.length() > 0) {
+ contentResolver.openOutputStream(document.uri, "wt")?.use { output ->
+ srcFile.inputStream().use { input ->
+ input.copyTo(output)
+ }
+ }
+ srcFile.delete()
+ }
+ } catch (e: Exception) {
+ android.util.Log.w("SpotiFLAC", "Failed to copy extension output to SAF: ${e.message}")
+ }
+ }
respObj.put("file_path", document.uri.toString())
respObj.put("file_name", document.name ?: fileName)
} else {
@@ -786,6 +807,72 @@ class MainActivity: FlutterFragmentActivity() {
}
}
+ /**
+ * Get the parent DocumentFile directory for a SAF document URI.
+ * The child URI must be a tree-based document URI (e.g. from SAF tree scan).
+ * Returns a DocumentFile that supports findFile() for sibling lookup.
+ */
+ private fun safParentDir(childUri: Uri): DocumentFile? {
+ try {
+ val docId = android.provider.DocumentsContract.getDocumentId(childUri)
+ if (docId.isNullOrEmpty()) return null
+
+ // Document IDs typically look like "primary:Music/Album/file.cue"
+ // Parent would be "primary:Music/Album"
+ val lastSlash = docId.lastIndexOf('/')
+ if (lastSlash <= 0) return null
+
+ val parentDocId = docId.substring(0, lastSlash)
+
+ // Build a tree document URI for the parent so it supports listing/findFile
+ val treeDocId = android.provider.DocumentsContract.getTreeDocumentId(childUri)
+ if (treeDocId.isNullOrEmpty()) return null
+
+ val parentUri = android.provider.DocumentsContract.buildDocumentUriUsingTree(
+ childUri, parentDocId
+ )
+ return DocumentFile.fromTreeUri(this, parentUri)
+ ?: DocumentFile.fromSingleUri(this, parentUri)
+ } catch (e: Exception) {
+ android.util.Log.w("SpotiFLAC", "Failed to get SAF parent dir: ${e.message}")
+ return null
+ }
+ }
+
+ /**
+ * Extract the audio filename referenced by a CUE sheet file.
+ * Reads the FILE "name" TYPE line from the .cue text.
+ * Returns just the filename (no path), or null if not found.
+ */
+ private fun extractCueAudioFileName(cueTempPath: String): String? {
+ try {
+ val lines = File(cueTempPath).readLines()
+ for (line in lines) {
+ val trimmed = line.trim().let { l ->
+ // Strip BOM
+ if (l.startsWith("\uFEFF")) l.removePrefix("\uFEFF").trim() else l
+ }
+ if (trimmed.uppercase(Locale.ROOT).startsWith("FILE ")) {
+ val rest = trimmed.substring(5).trim()
+ // Parse: "filename" TYPE or filename TYPE
+ val filename = if (rest.startsWith("\"")) {
+ val endQuote = rest.indexOf('"', 1)
+ if (endQuote > 0) rest.substring(1, endQuote) else rest
+ } else {
+ // Last word is the type, everything else is the filename
+ val parts = rest.split("\\s+".toRegex())
+ if (parts.size >= 2) parts.dropLast(1).joinToString(" ") else rest
+ }
+ // Return just the filename (strip any path separators)
+ return filename.substringAfterLast("/").substringAfterLast("\\")
+ }
+ }
+ } catch (e: Exception) {
+ android.util.Log.w("SpotiFLAC", "Failed to extract audio filename from CUE: ${e.message}")
+ }
+ return null
+ }
+
private fun scanSafTree(treeUriStr: String): String {
if (treeUriStr.isBlank()) return "[]"
@@ -799,8 +886,10 @@ class MainActivity: FlutterFragmentActivity() {
it.currentFile = "Scanning folders..."
}
- val supportedExt = setOf(".flac", ".m4a", ".mp3", ".opus", ".ogg")
+ val supportedAudioExt = setOf(".flac", ".m4a", ".mp3", ".opus", ".ogg")
val audioFiles = mutableListOf>()
+ // CUE files: (cueDoc, parentDir) — we need the parent to find sibling audio
+ val cueFiles = mutableListOf>()
val visitedDirUris = mutableSetOf()
var traversalErrors = 0
@@ -849,7 +938,9 @@ class MainActivity: FlutterFragmentActivity() {
} else if (child.isFile) {
val name = child.name ?: continue
val ext = name.substringAfterLast('.', "").lowercase(Locale.ROOT)
- if (ext.isNotBlank() && supportedExt.contains(".$ext")) {
+ if (ext == "cue") {
+ cueFiles.add(child to dir)
+ } else if (ext.isNotBlank() && supportedAudioExt.contains(".$ext")) {
audioFiles.add(child to path)
}
}
@@ -864,11 +955,12 @@ class MainActivity: FlutterFragmentActivity() {
}
}
+ val totalItems = audioFiles.size + cueFiles.size
updateSafScanProgress {
- it.totalFiles = audioFiles.size
+ it.totalFiles = totalItems
}
- if (audioFiles.isEmpty()) {
+ if (audioFiles.isEmpty() && cueFiles.isEmpty()) {
updateSafScanProgress {
it.isComplete = true
it.progressPct = 100.0
@@ -880,12 +972,138 @@ class MainActivity: FlutterFragmentActivity() {
var scanned = 0
var errors = traversalErrors
+ // --- CUE first pass: parse CUE sheets, expand to tracks, track referenced audio ---
+ val cueReferencedAudioUris = mutableSetOf()
+
+ for ((cueDoc, parentDir) in cueFiles) {
+ if (safScanCancel) {
+ updateSafScanProgress { it.isComplete = true }
+ return "[]"
+ }
+
+ val cueName = try { cueDoc.name ?: "" } catch (_: Exception) { "" }
+ updateSafScanProgress { it.currentFile = cueName }
+
+ var tempCuePath: String? = null
+ var tempAudioPath: String? = null
+ try {
+ // Copy CUE to temp
+ tempCuePath = copyUriToTemp(cueDoc.uri, ".cue")
+ if (tempCuePath == null) {
+ errors++
+ android.util.Log.w("SpotiFLAC", "SAF scan: failed to copy CUE ${cueDoc.uri}")
+ scanned++
+ continue
+ }
+
+ // Extract the audio filename from the CUE sheet text
+ val audioFileName = extractCueAudioFileName(tempCuePath)
+
+ // Find the referenced audio file as a sibling in the same SAF directory
+ var audioDoc: DocumentFile? = null
+ if (!audioFileName.isNullOrBlank()) {
+ audioDoc = try { parentDir.findFile(audioFileName) } catch (_: Exception) { null }
+ }
+
+ // Fallback: try common audio extensions with the CUE base name
+ if (audioDoc == null) {
+ val cueBaseName = cueName.substringBeforeLast('.')
+ val commonExts = listOf(".flac", ".wav", ".ape", ".mp3", ".ogg", ".wv", ".m4a")
+ for (ext in commonExts) {
+ audioDoc = try { parentDir.findFile(cueBaseName + ext) } catch (_: Exception) { null }
+ if (audioDoc != null) break
+ // Try uppercase
+ audioDoc = try { parentDir.findFile(cueBaseName + ext.uppercase(Locale.ROOT)) } catch (_: Exception) { null }
+ if (audioDoc != null) break
+ }
+ }
+
+ if (audioDoc == null) {
+ android.util.Log.w("SpotiFLAC", "SAF scan: no audio file found for CUE $cueName")
+ errors++
+ scanned++
+ continue
+ }
+
+ // Mark this audio file so we skip it in the regular audio pass
+ cueReferencedAudioUris.add(audioDoc.uri.toString())
+
+ // Copy audio to same temp dir so Go can resolve it
+ val tempDir = File(tempCuePath).parent ?: cacheDir.absolutePath
+ val audioName = try { audioDoc.name ?: "audio.flac" } catch (_: Exception) { "audio.flac" }
+ val audioExt = audioName.substringAfterLast('.', "").lowercase(Locale.ROOT)
+ val fallbackAudioExt = if (audioExt.isNotBlank()) ".$audioExt" else null
+
+ tempAudioPath = copyUriToTemp(audioDoc.uri, fallbackAudioExt)
+ if (tempAudioPath == null) {
+ android.util.Log.w("SpotiFLAC", "SAF scan: failed to copy audio for CUE $cueName")
+ errors++
+ scanned++
+ continue
+ }
+
+ // Rename temp audio to its original name so Go can find it by name
+ val renamedAudio = File(tempDir, audioName)
+ val tempAudioFile = File(tempAudioPath)
+ if (renamedAudio.absolutePath != tempAudioFile.absolutePath) {
+ tempAudioFile.renameTo(renamedAudio)
+ tempAudioPath = renamedAudio.absolutePath
+ }
+
+ val cueLastModified = try { cueDoc.lastModified() } catch (_: Exception) { 0L }
+
+ // Call Go to produce library scan entries for each CUE track
+ val cueResultsJson = Gobackend.scanCueSheetForLibrary(
+ tempCuePath,
+ tempDir,
+ cueDoc.uri.toString(),
+ cueLastModified
+ )
+
+ val cueArray = JSONArray(cueResultsJson)
+ for (j in 0 until cueArray.length()) {
+ results.put(cueArray.getJSONObject(j))
+ }
+
+ android.util.Log.d(
+ "SpotiFLAC",
+ "SAF scan: CUE $cueName -> ${cueArray.length()} tracks"
+ )
+ } catch (e: Exception) {
+ errors++
+ android.util.Log.w("SpotiFLAC", "SAF scan: error processing CUE $cueName: ${e.message}")
+ } finally {
+ try { tempCuePath?.let { File(it).delete() } } catch (_: Exception) {}
+ try { tempAudioPath?.let { File(it).delete() } } catch (_: Exception) {}
+ }
+
+ scanned++
+ val pct = scanned.toDouble() / totalItems.toDouble() * 100.0
+ updateSafScanProgress {
+ it.scannedFiles = scanned
+ it.errorCount = errors
+ it.progressPct = pct
+ }
+ }
+
+ // --- Regular audio file pass: skip files referenced by CUE sheets ---
for ((doc, _) in audioFiles) {
if (safScanCancel) {
updateSafScanProgress { it.isComplete = true }
return "[]"
}
+ // Skip audio files that are represented by CUE track entries
+ if (cueReferencedAudioUris.contains(doc.uri.toString())) {
+ scanned++
+ val pct = scanned.toDouble() / totalItems.toDouble() * 100.0
+ updateSafScanProgress {
+ it.scannedFiles = scanned
+ it.progressPct = pct
+ }
+ continue
+ }
+
val name = try { doc.name ?: "" } catch (_: Exception) { "" }
updateSafScanProgress {
it.currentFile = name
@@ -926,7 +1144,7 @@ class MainActivity: FlutterFragmentActivity() {
}
scanned++
- val pct = scanned.toDouble() / audioFiles.size.toDouble() * 100.0
+ val pct = scanned.toDouble() / totalItems.toDouble() * 100.0
updateSafScanProgress {
it.scannedFiles = scanned
it.errorCount = errors
@@ -944,6 +1162,8 @@ class MainActivity: FlutterFragmentActivity() {
/**
* Incremental SAF tree scan - only scans new or modified files.
+ * Supports .cue sheets: expands them into virtual track entries and
+ * deduplicates audio files referenced by CUE sheets.
* @param treeUriStr The SAF tree URI to scan
* @param existingFilesJson JSON object mapping file URI -> lastModified timestamp
* @return JSON object with new/changed files and removed URIs
@@ -986,13 +1206,29 @@ class MainActivity: FlutterFragmentActivity() {
it.currentFile = "Scanning folders..."
}
- val supportedExt = setOf(".flac", ".m4a", ".mp3", ".opus", ".ogg")
+ val supportedAudioExt = setOf(".flac", ".m4a", ".mp3", ".opus", ".ogg")
val audioFiles = mutableListOf>() // doc, path, lastModified
+ // CUE files to scan: (cueDoc, parentDir, lastModified)
+ val cueFilesToScan = mutableListOf>()
+ // Unchanged CUE files: (cueDoc, parentDir) — need to discover audio siblings for skip set
+ val unchangedCueFiles = mutableListOf>()
val currentUris = mutableSetOf()
val visitedDirUris = mutableSetOf()
var traversalErrors = 0
- // Collect all audio files with lastModified
+ // Build a map of CUE base URIs -> existing virtual track URIs from the database.
+ // Virtual paths look like "content://...album.cue#track01".
+ // We need this to preserve virtual paths for unchanged CUE files.
+ val existingCueVirtualPaths = mutableMapOf>() // cueUri -> [virtualPaths]
+ for (key in existingFiles.keys) {
+ val hashIdx = key.indexOf("#track")
+ if (hashIdx > 0) {
+ val baseCueUri = key.substring(0, hashIdx)
+ existingCueVirtualPaths.getOrPut(baseCueUri) { mutableListOf() }.add(key)
+ }
+ }
+
+ // Collect all files with lastModified
val queue: ArrayDeque> = ArrayDeque()
queue.add(root to "")
@@ -1055,7 +1291,27 @@ class MainActivity: FlutterFragmentActivity() {
val name = child.name ?: continue
val ext = name.substringAfterLast('.', "").lowercase(Locale.ROOT)
- if (ext.isNotBlank() && supportedExt.contains(".$ext")) {
+
+ if (ext == "cue") {
+ val lastModified = try {
+ child.lastModified()
+ } catch (_: Exception) { 0L }
+
+ // Check if any virtual track from this CUE exists with matching modTime
+ val virtualPaths = existingCueVirtualPaths[uriStr]
+ val existingModified = virtualPaths?.firstOrNull()?.let { existingFiles[it] }
+
+ if (existingModified != null && existingModified == lastModified) {
+ // CUE is unchanged — mark virtual paths as current so they aren't removed
+ unchangedCueFiles.add(child to dir)
+ for (vp in virtualPaths) {
+ currentUris.add(vp)
+ }
+ } else {
+ // CUE is new or modified — needs scanning
+ cueFilesToScan.add(Triple(child, dir, lastModified))
+ }
+ } else if (ext.isNotBlank() && supportedAudioExt.contains(".$ext")) {
val existingModified = existingFiles[uriStr]
val lastModified = try {
child.lastModified()
@@ -1083,13 +1339,14 @@ class MainActivity: FlutterFragmentActivity() {
// Find removed files (in existing but not in current)
val removedUris = existingFiles.keys.filter { !currentUris.contains(it) }
val totalFiles = currentUris.size
- val skippedCount = (totalFiles - audioFiles.size).coerceAtLeast(0)
+ val filesToProcess = audioFiles.size + cueFilesToScan.size
+ val skippedCount = (totalFiles - filesToProcess).coerceAtLeast(0)
updateSafScanProgress {
it.totalFiles = totalFiles
}
- if (audioFiles.isEmpty()) {
+ if (audioFiles.isEmpty() && cueFilesToScan.isEmpty()) {
updateSafScanProgress {
it.isComplete = true
it.scannedFiles = totalFiles
@@ -1107,6 +1364,173 @@ class MainActivity: FlutterFragmentActivity() {
var scanned = 0
var errors = traversalErrors
+ // --- CUE first pass: parse new/modified CUE sheets ---
+ val cueReferencedAudioUris = mutableSetOf()
+
+ for ((cueDoc, parentDir, cueLastModified) in cueFilesToScan) {
+ if (safScanCancel) {
+ updateSafScanProgress { it.isComplete = true }
+ val result = JSONObject()
+ result.put("files", JSONArray())
+ result.put("removedUris", JSONArray())
+ result.put("skippedCount", skippedCount)
+ result.put("totalFiles", totalFiles)
+ result.put("cancelled", true)
+ return result.toString()
+ }
+
+ val cueName = try { cueDoc.name ?: "" } catch (_: Exception) { "" }
+ updateSafScanProgress { it.currentFile = cueName }
+
+ var tempCuePath: String? = null
+ var tempAudioPath: String? = null
+ try {
+ // Copy CUE to temp
+ tempCuePath = copyUriToTemp(cueDoc.uri, ".cue")
+ if (tempCuePath == null) {
+ errors++
+ android.util.Log.w("SpotiFLAC", "SAF incremental scan: failed to copy CUE ${cueDoc.uri}")
+ scanned++
+ continue
+ }
+
+ // Extract the audio filename from the CUE sheet text
+ val audioFileName = extractCueAudioFileName(tempCuePath)
+
+ // Find the referenced audio file as a sibling in the same SAF directory
+ var audioDoc: DocumentFile? = null
+ if (!audioFileName.isNullOrBlank()) {
+ audioDoc = try { parentDir.findFile(audioFileName) } catch (_: Exception) { null }
+ }
+
+ // Fallback: try common audio extensions with the CUE base name
+ if (audioDoc == null) {
+ val cueBaseName = cueName.substringBeforeLast('.')
+ val commonExts = listOf(".flac", ".wav", ".ape", ".mp3", ".ogg", ".wv", ".m4a")
+ for (ext in commonExts) {
+ audioDoc = try { parentDir.findFile(cueBaseName + ext) } catch (_: Exception) { null }
+ if (audioDoc != null) break
+ audioDoc = try { parentDir.findFile(cueBaseName + ext.uppercase(Locale.ROOT)) } catch (_: Exception) { null }
+ if (audioDoc != null) break
+ }
+ }
+
+ if (audioDoc == null) {
+ android.util.Log.w("SpotiFLAC", "SAF incremental scan: no audio file found for CUE $cueName")
+ errors++
+ scanned++
+ continue
+ }
+
+ // Mark this audio file so we skip it in the regular audio pass
+ cueReferencedAudioUris.add(audioDoc.uri.toString())
+
+ // Copy audio to same temp dir so Go can resolve it
+ val tempDir = File(tempCuePath).parent ?: cacheDir.absolutePath
+ val audioName = try { audioDoc.name ?: "audio.flac" } catch (_: Exception) { "audio.flac" }
+ val audioExt = audioName.substringAfterLast('.', "").lowercase(Locale.ROOT)
+ val fallbackAudioExt = if (audioExt.isNotBlank()) ".$audioExt" else null
+
+ tempAudioPath = copyUriToTemp(audioDoc.uri, fallbackAudioExt)
+ if (tempAudioPath == null) {
+ android.util.Log.w("SpotiFLAC", "SAF incremental scan: failed to copy audio for CUE $cueName")
+ errors++
+ scanned++
+ continue
+ }
+
+ // Rename temp audio to its original name so Go can find it by name
+ val renamedAudio = File(tempDir, audioName)
+ val tempAudioFile = File(tempAudioPath)
+ if (renamedAudio.absolutePath != tempAudioFile.absolutePath) {
+ tempAudioFile.renameTo(renamedAudio)
+ tempAudioPath = renamedAudio.absolutePath
+ }
+
+ // Call Go to produce library scan entries for each CUE track
+ val cueResultsJson = Gobackend.scanCueSheetForLibrary(
+ tempCuePath,
+ tempDir,
+ cueDoc.uri.toString(),
+ cueLastModified
+ )
+
+ val cueArray = JSONArray(cueResultsJson)
+ for (j in 0 until cueArray.length()) {
+ val trackObj = cueArray.getJSONObject(j)
+ results.put(trackObj)
+ // Register each virtual path as current so deletion detection works
+ val virtualPath = trackObj.optString("filePath", "")
+ if (virtualPath.isNotBlank()) {
+ currentUris.add(virtualPath)
+ }
+ }
+
+ android.util.Log.d(
+ "SpotiFLAC",
+ "SAF incremental scan: CUE $cueName -> ${cueArray.length()} tracks"
+ )
+ } catch (e: Exception) {
+ errors++
+ android.util.Log.w("SpotiFLAC", "SAF incremental scan: error processing CUE $cueName: ${e.message}")
+ } finally {
+ try { tempCuePath?.let { File(it).delete() } } catch (_: Exception) {}
+ try { tempAudioPath?.let { File(it).delete() } } catch (_: Exception) {}
+ }
+
+ scanned++
+ val processed = skippedCount + scanned
+ val pct = if (totalFiles > 0) {
+ processed.toDouble() / totalFiles.toDouble() * 100.0
+ } else {
+ 100.0
+ }
+ updateSafScanProgress {
+ it.scannedFiles = processed
+ it.errorCount = errors
+ it.progressPct = pct
+ }
+ }
+
+ // Discover audio siblings for unchanged CUE files so we skip them
+ // in the regular audio pass. Copy the .cue to temp (tiny file) to extract
+ // the audio filename, then find the sibling by name.
+ for ((cueDoc, parentDir) in unchangedCueFiles) {
+ var tempCue: String? = null
+ try {
+ tempCue = copyUriToTemp(cueDoc.uri, ".cue")
+ if (tempCue != null) {
+ val audioFileName = extractCueAudioFileName(tempCue)
+ var audioDoc: DocumentFile? = null
+ if (!audioFileName.isNullOrBlank()) {
+ audioDoc = try { parentDir.findFile(audioFileName) } catch (_: Exception) { null }
+ }
+ // Fallback: try common extensions with CUE base name
+ if (audioDoc == null) {
+ val cueName = try { cueDoc.name ?: "" } catch (_: Exception) { "" }
+ val cueBaseName = cueName.substringBeforeLast('.')
+ if (cueBaseName.isNotBlank()) {
+ val commonExts = listOf(".flac", ".wav", ".ape", ".mp3", ".ogg", ".wv", ".m4a")
+ for (ext in commonExts) {
+ audioDoc = try { parentDir.findFile(cueBaseName + ext) } catch (_: Exception) { null }
+ if (audioDoc != null) break
+ audioDoc = try { parentDir.findFile(cueBaseName + ext.uppercase(Locale.ROOT)) } catch (_: Exception) { null }
+ if (audioDoc != null) break
+ }
+ }
+ }
+ if (audioDoc != null) {
+ cueReferencedAudioUris.add(audioDoc.uri.toString())
+ }
+ }
+ } catch (e: Exception) {
+ android.util.Log.w("SpotiFLAC", "SAF incremental scan: failed to resolve audio for unchanged CUE: ${e.message}")
+ } finally {
+ try { tempCue?.let { File(it).delete() } } catch (_: Exception) {}
+ }
+ }
+
+ // --- Regular audio file pass: skip files referenced by CUE sheets ---
for ((doc, _, lastModified) in audioFiles) {
if (safScanCancel) {
updateSafScanProgress { it.isComplete = true }
@@ -1119,6 +1543,22 @@ class MainActivity: FlutterFragmentActivity() {
return result.toString()
}
+ // Skip audio files that are represented by CUE track entries
+ if (cueReferencedAudioUris.contains(doc.uri.toString())) {
+ scanned++
+ val processed = skippedCount + scanned
+ val pct = if (totalFiles > 0) {
+ processed.toDouble() / totalFiles.toDouble() * 100.0
+ } else {
+ 100.0
+ }
+ updateSafScanProgress {
+ it.scannedFiles = processed
+ it.progressPct = pct
+ }
+ continue
+ }
+
val name = try { doc.name ?: "" } catch (_: Exception) { "" }
updateSafScanProgress {
it.currentFile = name
@@ -1173,6 +1613,9 @@ class MainActivity: FlutterFragmentActivity() {
}
}
+ // Recalculate removedUris now that CUE virtual paths have been registered
+ val finalRemovedUris = existingFiles.keys.filter { !currentUris.contains(it) }
+
updateSafScanProgress {
it.isComplete = true
it.progressPct = 100.0
@@ -1180,7 +1623,7 @@ class MainActivity: FlutterFragmentActivity() {
val result = JSONObject()
result.put("files", results)
- result.put("removedUris", JSONArray(removedUris))
+ result.put("removedUris", JSONArray(finalRemovedUris))
result.put("skippedCount", skippedCount)
result.put("totalFiles", totalFiles)
return result.toString()
@@ -1434,38 +1877,6 @@ class MainActivity: FlutterFragmentActivity() {
}
result.success(response)
}
- "getSpotifyMetadata" -> {
- val url = call.argument("url") ?: ""
- val response = withContext(Dispatchers.IO) {
- Gobackend.getSpotifyMetadata(url)
- }
- result.success(response)
- }
- "searchSpotify" -> {
- val query = call.argument("query") ?: ""
- val limit = call.argument("limit") ?: 10
- val response = withContext(Dispatchers.IO) {
- Gobackend.searchSpotify(query, limit.toLong())
- }
- result.success(response)
- }
- "searchSpotifyAll" -> {
- val query = call.argument("query") ?: ""
- val trackLimit = call.argument("track_limit") ?: 15
- val artistLimit = call.argument("artist_limit") ?: 3
- val response = withContext(Dispatchers.IO) {
- Gobackend.searchSpotifyAll(query, trackLimit.toLong(), artistLimit.toLong())
- }
- result.success(response)
- }
- "getSpotifyRelatedArtists" -> {
- val artistId = call.argument("artist_id") ?: ""
- val limit = call.argument("limit") ?: 12
- val response = withContext(Dispatchers.IO) {
- Gobackend.getSpotifyRelatedArtists(artistId, limit.toLong())
- }
- result.success(response)
- }
"checkAvailability" -> {
val spotifyId = call.argument("spotify_id") ?: ""
val isrc = call.argument("isrc") ?: ""
@@ -2099,20 +2510,6 @@ class MainActivity: FlutterFragmentActivity() {
"isDownloadServiceRunning" -> {
result.success(DownloadService.isServiceRunning())
}
- "setSpotifyCredentials" -> {
- val clientId = call.argument("client_id") ?: ""
- val clientSecret = call.argument("client_secret") ?: ""
- withContext(Dispatchers.IO) {
- Gobackend.setSpotifyAPICredentials(clientId, clientSecret)
- }
- result.success(null)
- }
- "hasSpotifyCredentials" -> {
- val hasCredentials = withContext(Dispatchers.IO) {
- Gobackend.checkSpotifyCredentials()
- }
- result.success(hasCredentials)
- }
"preWarmTrackCache" -> {
val tracksJson = call.argument("tracks") ?: "[]"
withContext(Dispatchers.IO) {
@@ -2239,13 +2636,6 @@ class MainActivity: FlutterFragmentActivity() {
}
result.success(response)
}
- "getAmazonURLFromDeezerTrack" -> {
- val deezerTrackId = call.argument("deezer_track_id") ?: ""
- val response = withContext(Dispatchers.IO) {
- Gobackend.getAmazonURLFromDeezerTrack(deezerTrackId)
- }
- result.success(response)
- }
// Log methods
"getLogs" -> {
val response = withContext(Dispatchers.IO) {
@@ -2742,6 +3132,89 @@ class MainActivity: FlutterFragmentActivity() {
}
result.success(response)
}
+ // CUE Sheet Parsing
+ "parseCueSheet" -> {
+ val cuePath = call.argument("cue_path") ?: ""
+ val audioDir = call.argument("audio_dir") ?: ""
+ val response = withContext(Dispatchers.IO) {
+ try {
+ if (cuePath.startsWith("content://")) {
+ val uri = Uri.parse(cuePath)
+ val tempCuePath = copyUriToTemp(uri, ".cue")
+ ?: return@withContext """{"error":"Failed to copy CUE file to temp"}"""
+ var tempAudioPath: String? = null
+ try {
+ // Extract audio filename from CUE text
+ val audioFileName = extractCueAudioFileName(tempCuePath)
+
+ // Try to find the audio sibling in SAF
+ var audioDoc: DocumentFile? = null
+ val parentDir = safParentDir(uri)
+ if (parentDir != null && !audioFileName.isNullOrBlank()) {
+ audioDoc = try { parentDir.findFile(audioFileName) } catch (_: Exception) { null }
+ }
+
+ // Fallback: try common extensions with the CUE base name
+ if (audioDoc == null && parentDir != null) {
+ val cueName = try {
+ DocumentFile.fromSingleUri(this@MainActivity, uri)?.name ?: ""
+ } catch (_: Exception) { "" }
+ val cueBaseName = cueName.substringBeforeLast('.')
+ if (cueBaseName.isNotBlank()) {
+ val commonExts = listOf(".flac", ".wav", ".ape", ".mp3", ".ogg", ".wv", ".m4a")
+ for (ext in commonExts) {
+ audioDoc = try { parentDir.findFile(cueBaseName + ext) } catch (_: Exception) { null }
+ if (audioDoc != null) break
+ audioDoc = try { parentDir.findFile(cueBaseName + ext.uppercase(Locale.ROOT)) } catch (_: Exception) { null }
+ if (audioDoc != null) break
+ }
+ }
+ }
+
+ val tempDir = File(tempCuePath).parent ?: cacheDir.absolutePath
+ if (audioDoc != null) {
+ // Copy audio to same temp dir with original name
+ val audioName = try { audioDoc.name ?: "audio.flac" } catch (_: Exception) { "audio.flac" }
+ val audioExt = audioName.substringAfterLast('.', "").lowercase(Locale.ROOT)
+ val fallbackExt = if (audioExt.isNotBlank()) ".$audioExt" else null
+ val copiedAudio = copyUriToTemp(audioDoc.uri, fallbackExt)
+ if (copiedAudio != null) {
+ val renamedAudio = File(tempDir, audioName)
+ val copiedFile = File(copiedAudio)
+ if (renamedAudio.absolutePath != copiedFile.absolutePath) {
+ copiedFile.renameTo(renamedAudio)
+ }
+ tempAudioPath = renamedAudio.absolutePath
+ }
+ }
+
+ // Parse with audio in temp dir; Go will resolve there
+ val resultJson = Gobackend.parseCueSheet(tempCuePath, tempDir)
+
+ // Replace the temp audio_path with the SAF content:// URI
+ // so Dart knows it's a SAF file and handles it accordingly
+ if (audioDoc != null) {
+ val resultObj = JSONObject(resultJson)
+ resultObj.put("audio_path", audioDoc.uri.toString())
+ // Also pass the original CUE URI for reference
+ resultObj.put("cue_path", cuePath)
+ resultObj.toString()
+ } else {
+ resultJson
+ }
+ } finally {
+ try { File(tempCuePath).delete() } catch (_: Exception) {}
+ try { tempAudioPath?.let { File(it).delete() } } catch (_: Exception) {}
+ }
+ } else {
+ Gobackend.parseCueSheet(cuePath, audioDir)
+ }
+ } catch (e: Exception) {
+ """{"error":"${e.message?.replace("\"", "'")}"}"""
+ }
+ }
+ result.success(response)
+ }
else -> result.notImplemented()
}
} catch (e: Exception) {
diff --git a/assets/images/banner-readme-dark.png b/assets/images/banner-readme-dark.png
new file mode 100644
index 00000000..13c40d0b
Binary files /dev/null and b/assets/images/banner-readme-dark.png differ
diff --git a/assets/images/banner-readme-light.png b/assets/images/banner-readme-light.png
new file mode 100644
index 00000000..2aaaba69
Binary files /dev/null and b/assets/images/banner-readme-light.png differ
diff --git a/cliff.toml b/cliff.toml
new file mode 100644
index 00000000..0509818b
--- /dev/null
+++ b/cliff.toml
@@ -0,0 +1,105 @@
+# git-cliff configuration for SpotiFLAC Mobile
+# https://git-cliff.org/docs/configuration
+
+[changelog]
+# Template for the changelog body
+body = """
+{%- macro remote_url() -%}
+ https://github.com/zarzet/SpotiFLAC-Mobile
+{%- endmacro -%}
+
+{% if version %}\
+ ## {{ version | trim_start_matches(pat="v") }}
+{% else %}\
+ ## Unreleased
+{% endif %}\
+
+{% for group, commits in commits | group_by(attribute="group") %}
+ ### {{ group | striptags | trim | upper_first }}
+ {% for commit in commits %}
+ - {% if commit.scope %}**{{ commit.scope }}**: {% endif %}\
+ {{ commit.message | upper_first }}\
+ {% if commit.github.pr_number %} \
+ ([#{{ commit.github.pr_number }}]({{ self::remote_url() }}/pull/{{ commit.github.pr_number }}))\
+ {% endif %}\
+ {%- if commit.github.username %} by [@{{ commit.github.username }}](https://github.com/{{ commit.github.username }}){%- endif %}
+ {%- endfor %}
+{% endfor %}
+
+{%- if github.contributors | filter(attribute="is_first_time", value=true) | length != 0 %}
+
+ ### New Contributors
+{%- for contributor in github.contributors | filter(attribute="is_first_time", value=true) %}
+ * @{{ contributor.username }} made their first contribution
+ {%- if contributor.pr_number %} in \
+ [#{{ contributor.pr_number }}]({{ self::remote_url() }}/pull/{{ contributor.pr_number }}) \
+ {%- endif %}
+{%- endfor %}
+{%- endif -%}
+
+{% if version %}
+ {% if previous.version %}
+ **Full Changelog**: [{{ previous.version }}...{{ version }}]({{ self::remote_url() }}/compare/{{ previous.version }}...{{ version }})
+ {% endif %}
+{% else -%}
+ {% raw %}\n{% endraw %}
+{% endif %}
+"""
+# Remove leading and trailing whitespace
+trim = true
+
+[git]
+# Parse conventional commits
+conventional_commits = true
+filter_unconventional = true
+
+# Process each line of a commit as an individual commit
+split_commits = false
+
+# Regex for preprocessing the commit messages
+commit_preprocessors = [
+ # Remove PR number from message (we add it back via GitHub integration)
+ { pattern = '\(#(\d+)\)', replace = '' },
+ # Strip conventional commit prefix for cleaner messages
+ # (group header already shows the type)
+]
+
+# Regex for parsing and grouping commits
+commit_parsers = [
+ # Skip noise: translation commits from Crowdin
+ { message = "^New translations", skip = true },
+ { message = "^Update source file", skip = true },
+ # Skip merge commits
+ { message = "^Merge", skip = true },
+ # Skip version bump commits
+ { message = "^v\\d+", skip = true },
+ { message = "^chore: update VirusTotal", skip = true },
+
+ # Group by conventional commit type
+ { message = "^feat", group = "New Features" },
+ { message = "^fix", group = "Bug Fixes" },
+ { message = "^perf", group = "Performance" },
+ { message = "^refactor", group = "Refactoring" },
+ { message = "^doc", group = "Documentation" },
+ { message = "^style", group = "Styling" },
+ { message = "^test", group = "Testing" },
+ { message = "^chore\\(deps\\)", group = "Dependencies" },
+ { message = "^chore\\(l10n\\)", skip = true },
+ { message = "^chore|^ci", group = "Chores" },
+]
+
+# Protect breaking changes from being skipped
+protect_breaking_commits = true
+
+# Filter out commits by matching patterns
+filter_commits = false
+
+# Tag pattern for version detection
+tag_pattern = "v[0-9].*"
+
+# Sort commits by newest first
+sort_commits = "newest"
+
+[remote.github]
+owner = "zarzet"
+repo = "SpotiFLAC-Mobile"
diff --git a/go_backend/amazon.go b/go_backend/amazon.go
deleted file mode 100644
index b30d669d..00000000
--- a/go_backend/amazon.go
+++ /dev/null
@@ -1,692 +0,0 @@
-package gobackend
-
-import (
- "bufio"
- "context"
- "encoding/json"
- "errors"
- "fmt"
- "io"
- "net/http"
- "net/url"
- "os"
- "path/filepath"
- "regexp"
- "strings"
- "sync"
- "time"
-)
-
-// Amazon API timeout and retry configuration for mobile networks
-const (
- amazonAPITimeoutMobile = 30 * time.Second // Longer timeout for unstable mobile networks
- amazonMaxRetries = 2 // Number of retry attempts
- amazonRetryDelay = 500 * time.Millisecond
-)
-
-type AmazonDownloader struct {
- client *http.Client
-}
-
-var (
- globalAmazonDownloader *AmazonDownloader
- amazonDownloaderOnce sync.Once
- amazonASINRegex = regexp.MustCompile(`(?i)^B[0-9A-Z]{9}$`)
- amazonASINFindRegex = regexp.MustCompile(`(?i)B[0-9A-Z]{9}`)
-)
-
-// AfkarXYZResponse is the response from AfkarXYZ API
-type AfkarXYZResponse struct {
- Success bool `json:"success"`
- Data struct {
- DirectLink string `json:"direct_link"`
- FileName string `json:"file_name"`
- FileSize int64 `json:"file_size"`
- } `json:"data"`
-}
-
-// AmazonStreamResponse is the new response format from amzn.afkarxyz.fun/api/track/{asin}
-type AmazonStreamResponse struct {
- StreamURL string `json:"streamUrl"`
- DecryptionKey string `json:"decryptionKey"`
-}
-
-func NewAmazonDownloader() *AmazonDownloader {
- amazonDownloaderOnce.Do(func() {
- globalAmazonDownloader = &AmazonDownloader{
- client: NewHTTPClientWithTimeout(120 * time.Second),
- }
- })
- return globalAmazonDownloader
-}
-
-// fetchAmazonURLWithRetry fetches from AfkarXYZ API with retry logic for mobile networks.
-// Returns downloadURL, suggested fileName, optional decryptionKey.
-func (a *AmazonDownloader) fetchAmazonURLWithRetry(amazonURL string) (string, string, string, error) {
- var lastErr error
- for attempt := 0; attempt <= amazonMaxRetries; attempt++ {
- if attempt > 0 {
- delay := amazonRetryDelay * time.Duration(1<<(attempt-1)) // Exponential backoff
- GoLog("[Amazon] Retry %d/%d after %v...\n", attempt, amazonMaxRetries, delay)
- time.Sleep(delay)
- }
-
- downloadURL, fileName, decryptionKey, err := a.doAfkarXYZRequest(amazonURL)
- if err == nil {
- return downloadURL, fileName, decryptionKey, nil
- }
-
- lastErr = err
- errStr := strings.ToLower(err.Error())
-
- // Check if error is retryable
- isRetryable := strings.Contains(errStr, "timeout") ||
- strings.Contains(errStr, "connection reset") ||
- strings.Contains(errStr, "connection refused") ||
- strings.Contains(errStr, "eof") ||
- strings.Contains(errStr, "status 5") ||
- strings.Contains(errStr, "status 429") ||
- strings.Contains(errStr, "http 429")
-
- if !isRetryable {
- return "", "", "", err
- }
-
- GoLog("[Amazon] Attempt %d failed (retryable): %v\n", attempt+1, err)
- }
-
- return "", "", "", fmt.Errorf("all %d attempts failed: %w", amazonMaxRetries+1, lastErr)
-}
-
-func normalizeAmazonASIN(candidate string) string {
- trimmed := strings.TrimSpace(candidate)
- if trimmed == "" {
- return ""
- }
-
- if decoded, err := url.QueryUnescape(trimmed); err == nil {
- trimmed = decoded
- }
-
- trimmed = strings.ToUpper(trimmed)
- if idx := strings.IndexAny(trimmed, "?#&/"); idx >= 0 {
- trimmed = trimmed[:idx]
- }
-
- if amazonASINRegex.MatchString(trimmed) {
- return trimmed
- }
-
- return ""
-}
-
-func extractAmazonASIN(amazonURL string) string {
- raw := strings.TrimSpace(amazonURL)
- if raw == "" {
- return ""
- }
-
- parsed, err := url.Parse(raw)
- if err == nil {
- query := parsed.Query()
-
- // Prefer track-level ASIN when URL also contains albumAsin.
- for _, key := range []string{"trackAsin", "trackasin", "trackASIN", "asin", "ASIN", "i"} {
- if asin := normalizeAmazonASIN(query.Get(key)); asin != "" {
- return asin
- }
- }
-
- path := strings.Trim(parsed.Path, "/")
- if path != "" {
- segments := strings.Split(path, "/")
-
- for i := 0; i < len(segments)-1; i++ {
- segment := strings.ToLower(strings.TrimSpace(segments[i]))
- if segment == "track" || segment == "tracks" {
- if asin := normalizeAmazonASIN(segments[i+1]); asin != "" {
- return asin
- }
- }
- }
-
- if asin := normalizeAmazonASIN(segments[len(segments)-1]); asin != "" {
- return asin
- }
- }
- }
-
- match := amazonASINFindRegex.FindString(strings.ToUpper(raw))
- return normalizeAmazonASIN(match)
-}
-
-// doAfkarXYZRequest performs a single request to Amazon API.
-// It tries new endpoint first, then falls back to legacy /convert endpoint.
-func (a *AmazonDownloader) doAfkarXYZRequest(amazonURL string) (string, string, string, error) {
- asin := extractAmazonASIN(amazonURL)
- if asin != "" {
- GoLog("[Amazon] Using ASIN: %s\n", asin)
- downloadURL, fileName, decryptKey, err := a.doAfkarXYZRequestNew(asin)
- if err == nil {
- return downloadURL, fileName, decryptKey, nil
- }
- GoLog("[Amazon] New API failed for ASIN %s, trying legacy endpoint: %v\n", asin, err)
- }
- return a.doAfkarXYZRequestLegacy(amazonURL)
-}
-
-func (a *AmazonDownloader) doAfkarXYZRequestNew(asin string) (string, string, string, error) {
- ctx, cancel := context.WithTimeout(context.Background(), amazonAPITimeoutMobile)
- defer cancel()
-
- apiURL := fmt.Sprintf("https://amzn.afkarxyz.fun/api/track/%s", asin)
- req, err := http.NewRequestWithContext(ctx, "GET", apiURL, nil)
- if err != nil {
- return "", "", "", fmt.Errorf("failed to create request: %w", err)
- }
-
- req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36")
-
- resp, err := a.client.Do(req)
- if err != nil {
- return "", "", "", fmt.Errorf("failed to call Amazon API: %w", err)
- }
- defer resp.Body.Close()
-
- body, readErr := io.ReadAll(resp.Body)
- if readErr != nil {
- return "", "", "", fmt.Errorf("failed to read response: %w", readErr)
- }
-
- if resp.StatusCode != 200 {
- return "", "", "", fmt.Errorf("Amazon API returned status %d", resp.StatusCode)
- }
-
- var apiResp AmazonStreamResponse
- if err := json.Unmarshal(body, &apiResp); err != nil {
- return "", "", "", fmt.Errorf("failed to decode response: %w", err)
- }
-
- if strings.TrimSpace(apiResp.StreamURL) == "" {
- return "", "", "", fmt.Errorf("Amazon API returned empty stream URL")
- }
-
- fileName := asin + ".m4a"
- return apiResp.StreamURL, fileName, strings.TrimSpace(apiResp.DecryptionKey), nil
-}
-
-func (a *AmazonDownloader) doAfkarXYZRequestLegacy(amazonURL string) (string, string, string, error) {
- ctx, cancel := context.WithTimeout(context.Background(), amazonAPITimeoutMobile)
- defer cancel()
-
- apiURL := "https://amzn.afkarxyz.fun/convert?url=" + url.QueryEscape(amazonURL)
- req, err := http.NewRequestWithContext(ctx, "GET", apiURL, nil)
- if err != nil {
- return "", "", "", fmt.Errorf("failed to create legacy request: %w", err)
- }
-
- req.Header.Set("User-Agent", getRandomUserAgent())
-
- resp, err := a.client.Do(req)
- if err != nil {
- return "", "", "", fmt.Errorf("failed to call legacy AfkarXYZ API: %w", err)
- }
- defer resp.Body.Close()
-
- if resp.StatusCode != 200 {
- return "", "", "", fmt.Errorf("legacy AfkarXYZ API returned status %d", resp.StatusCode)
- }
-
- body, err := io.ReadAll(resp.Body)
- if err != nil {
- return "", "", "", fmt.Errorf("failed to read legacy response: %w", err)
- }
-
- var apiResp AfkarXYZResponse
- if err := json.Unmarshal(body, &apiResp); err != nil {
- return "", "", "", fmt.Errorf("failed to decode legacy response: %w", err)
- }
-
- if !apiResp.Success || strings.TrimSpace(apiResp.Data.DirectLink) == "" {
- return "", "", "", fmt.Errorf("legacy AfkarXYZ API failed or no download link found")
- }
-
- fileName := apiResp.Data.FileName
- if fileName == "" {
- fileName = "track.flac"
- }
-
- reg := regexp.MustCompile(`[<>:"/\\|?*]`)
- fileName = reg.ReplaceAllString(fileName, "")
-
- return apiResp.Data.DirectLink, fileName, "", nil
-}
-
-func (a *AmazonDownloader) downloadFromAfkarXYZ(amazonURL string) (string, string, string, error) {
- GoLog("[Amazon] Fetching from AfkarXYZ API...\n")
-
- downloadURL, fileName, decryptionKey, err := a.fetchAmazonURLWithRetry(amazonURL)
- if err != nil {
- return "", "", "", err
- }
-
- if decryptionKey != "" {
- GoLog("[Amazon] AfkarXYZ returned encrypted stream (decryption key available)\n")
- }
- GoLog("[Amazon] AfkarXYZ returned: %s\n", fileName)
- return downloadURL, fileName, decryptionKey, nil
-}
-
-func (a *AmazonDownloader) DownloadFile(downloadURL, outputPath string, outputFD int, itemID string) error {
- ctx := context.Background()
-
- if itemID != "" {
- StartItemProgress(itemID)
- defer CompleteItemProgress(itemID)
- ctx = initDownloadCancel(itemID)
- defer clearDownloadCancel(itemID)
- }
-
- if isDownloadCancelled(itemID) {
- return ErrDownloadCancelled
- }
-
- req, err := http.NewRequestWithContext(ctx, "GET", downloadURL, nil)
- if err != nil {
- return fmt.Errorf("failed to create request: %w", err)
- }
-
- req.Header.Set("User-Agent", getRandomUserAgent())
-
- resp, err := a.client.Do(req)
- if err != nil {
- if isDownloadCancelled(itemID) {
- return ErrDownloadCancelled
- }
- return err
- }
- defer resp.Body.Close()
-
- if resp.StatusCode != 200 {
- return fmt.Errorf("download failed: HTTP %d", resp.StatusCode)
- }
-
- expectedSize := resp.ContentLength
- if expectedSize > 0 && itemID != "" {
- SetItemBytesTotal(itemID, expectedSize)
- }
-
- out, err := openOutputForWrite(outputPath, outputFD)
- if err != nil {
- return err
- }
-
- bufWriter := bufio.NewWriterSize(out, 256*1024)
-
- var written int64
- if itemID != "" {
- pw := NewItemProgressWriter(bufWriter, itemID)
- written, err = io.Copy(pw, resp.Body)
- } else {
- written, err = io.Copy(bufWriter, resp.Body)
- }
-
- flushErr := bufWriter.Flush()
- closeErr := out.Close()
-
- if err != nil {
- cleanupOutputOnError(outputPath, outputFD)
- if isDownloadCancelled(itemID) {
- return ErrDownloadCancelled
- }
- return fmt.Errorf("download interrupted: %w", err)
- }
- if flushErr != nil {
- cleanupOutputOnError(outputPath, outputFD)
- return fmt.Errorf("failed to flush buffer: %w", flushErr)
- }
- if closeErr != nil {
- cleanupOutputOnError(outputPath, outputFD)
- return fmt.Errorf("failed to close file: %w", closeErr)
- }
-
- if expectedSize > 0 && written != expectedSize {
- cleanupOutputOnError(outputPath, outputFD)
- return fmt.Errorf("incomplete download: expected %d bytes, got %d bytes", expectedSize, written)
- }
-
- GoLog("[Amazon] Downloaded: %.2f MB (Complete)\n", float64(written)/(1024*1024))
- return nil
-}
-
-// AmazonDownloadResult contains download result with quality info
-type AmazonDownloadResult struct {
- FilePath string
- BitDepth int
- SampleRate int
- Title string
- Artist string
- Album string
- ReleaseDate string
- TrackNumber int
- DiscNumber int
- ISRC string
- LyricsLRC string
- DecryptionKey string
-}
-
-func resolveAmazonURLForRequest(req DownloadRequest, logPrefix string) (string, error) {
- if strings.TrimSpace(logPrefix) == "" {
- logPrefix = "Amazon"
- }
-
- amazonURL := ""
- if req.ISRC != "" {
- if cached := GetTrackIDCache().Get(req.ISRC); cached != nil && cached.AmazonURL != "" {
- amazonURL = cached.AmazonURL
- GoLog("[%s] Cache hit! Using cached Amazon URL for ISRC %s\n", logPrefix, req.ISRC)
- }
- }
-
- if amazonURL != "" {
- return amazonURL, nil
- }
-
- songlink := NewSongLinkClient()
- var availability *TrackAvailability
- var err error
-
- deezerID := strings.TrimSpace(req.DeezerID)
- if prefixedDeezerID, found := strings.CutPrefix(req.SpotifyID, "deezer:"); found && strings.TrimSpace(prefixedDeezerID) != "" {
- deezerID = strings.TrimSpace(prefixedDeezerID)
- }
-
- if deezerID != "" {
- GoLog("[%s] Using Deezer ID for SongLink lookup: %s\n", logPrefix, deezerID)
- availability, err = songlink.CheckAvailabilityFromDeezer(deezerID)
- } else if req.SpotifyID != "" {
- availability, err = songlink.CheckTrackAvailability(req.SpotifyID, req.ISRC)
- } else {
- return "", fmt.Errorf("no valid Spotify or Deezer ID provided for Amazon lookup")
- }
-
- if err != nil {
- return "", fmt.Errorf("failed to check Amazon availability via SongLink: %w", err)
- }
-
- if availability == nil || !availability.Amazon || availability.AmazonURL == "" {
- return "", fmt.Errorf("track not available on Amazon Music (SongLink returned no Amazon URL)")
- }
-
- amazonURL = availability.AmazonURL
- if req.ISRC != "" {
- GetTrackIDCache().SetAmazonURL(req.ISRC, amazonURL)
- }
-
- return amazonURL, nil
-}
-
-func downloadFromAmazon(req DownloadRequest) (AmazonDownloadResult, error) {
- downloader := NewAmazonDownloader()
-
- isSafOutput := isFDOutput(req.OutputFD) || strings.TrimSpace(req.OutputPath) != ""
- if !isSafOutput {
- if existingFile, exists := checkISRCExistsInternal(req.OutputDir, req.ISRC); exists {
- return AmazonDownloadResult{FilePath: "EXISTS:" + existingFile}, nil
- }
- }
-
- amazonURL, err := resolveAmazonURLForRequest(req, "Amazon")
- if err != nil {
- return AmazonDownloadResult{}, err
- }
-
- if !isSafOutput && req.OutputDir != "." {
- if err := os.MkdirAll(req.OutputDir, 0755); err != nil {
- return AmazonDownloadResult{}, fmt.Errorf("failed to create output directory: %w", err)
- }
- }
-
- // Download using AfkarXYZ API
- downloadURL, afkarFileName, decryptionKey, err := downloader.downloadFromAfkarXYZ(amazonURL)
- if err != nil {
- return AmazonDownloadResult{}, fmt.Errorf("failed to get download URL from AfkarXYZ: %w", err)
- }
-
- GoLog("[Amazon] Match found: '%s' by '%s'\n", req.TrackName, req.ArtistName)
-
- filename := buildFilenameFromTemplate(req.FilenameFormat, map[string]any{
- "title": req.TrackName,
- "artist": req.ArtistName,
- "album": req.AlbumName,
- "track": req.TrackNumber,
- "year": extractYear(req.ReleaseDate),
- "date": req.ReleaseDate,
- "disc": req.DiscNumber,
- })
- var outputPath string
- if isSafOutput {
- outputPath = strings.TrimSpace(req.OutputPath)
- if outputPath == "" && isFDOutput(req.OutputFD) {
- outputPath = fmt.Sprintf("/proc/self/fd/%d", req.OutputFD)
- }
- } else {
- outputExt := strings.ToLower(filepath.Ext(afkarFileName))
- if outputExt == "" {
- outputExt = ".flac"
- }
- filename = sanitizeFilename(filename) + outputExt
- outputPath = filepath.Join(req.OutputDir, filename)
- if fileInfo, statErr := os.Stat(outputPath); statErr == nil && fileInfo.Size() > 0 {
- return AmazonDownloadResult{FilePath: "EXISTS:" + outputPath}, nil
- }
- }
-
- // START PARALLEL: Fetch cover and lyrics while downloading audio
- var parallelResult *ParallelDownloadResult
- parallelDone := make(chan struct{})
- go func() {
- defer close(parallelDone)
- coverURL := req.CoverURL
- embedLyrics := req.EmbedLyrics
- if !req.EmbedMetadata {
- coverURL = ""
- embedLyrics = false
- }
- parallelResult = FetchCoverAndLyricsParallel(
- coverURL,
- req.EmbedMaxQualityCover,
- req.SpotifyID,
- req.TrackName,
- req.ArtistName,
- embedLyrics,
- int64(req.DurationMS),
- )
- }()
-
- // Download audio file with item ID for progress tracking
- if err := downloader.DownloadFile(downloadURL, outputPath, req.OutputFD, req.ItemID); err != nil {
- if errors.Is(err, ErrDownloadCancelled) {
- return AmazonDownloadResult{}, ErrDownloadCancelled
- }
- return AmazonDownloadResult{}, fmt.Errorf("download failed: %w", err)
- }
-
- actualOutputPath := outputPath
- needsDecryption := strings.TrimSpace(decryptionKey) != ""
- if needsDecryption {
- GoLog("[Amazon] Download requires decryption; deferring decrypt to Flutter FFmpeg path\n")
- }
-
- // Wait for parallel operations to complete
- <-parallelDone
-
- if req.ItemID != "" {
- SetItemProgress(req.ItemID, 1.0, 0, 0)
- SetItemFinalizing(req.ItemID)
- }
-
- actualTrackNum := req.TrackNumber
- actualDiscNum := req.DiscNumber
- actualDate := req.ReleaseDate
- actualAlbum := req.AlbumName
- actualTitle := req.TrackName
- actualArtist := req.ArtistName
-
- if !needsDecryption {
- existingMeta, metaErr := ReadMetadata(actualOutputPath)
- if metaErr == nil && existingMeta != nil {
- if existingMeta.TrackNumber > 0 && (req.TrackNumber == 0 || req.TrackNumber == 1) {
- actualTrackNum = existingMeta.TrackNumber
- GoLog("[Amazon] Using track number from file: %d (request had: %d)\n", actualTrackNum, req.TrackNumber)
- }
- if existingMeta.DiscNumber > 0 && (req.DiscNumber == 0 || req.DiscNumber == 1) {
- actualDiscNum = existingMeta.DiscNumber
- GoLog("[Amazon] Using disc number from file: %d (request had: %d)\n", actualDiscNum, req.DiscNumber)
- }
- if existingMeta.Date != "" && req.ReleaseDate == "" {
- actualDate = existingMeta.Date
- GoLog("[Amazon] Using release date from file: %s\n", actualDate)
- }
- if existingMeta.Album != "" && req.AlbumName == "" {
- actualAlbum = existingMeta.Album
- GoLog("[Amazon] Using album from file: %s\n", actualAlbum)
- }
- GoLog("[Amazon] Existing metadata - Title: %s, Artist: %s, Album: %s, Date: %s\n",
- existingMeta.Title, existingMeta.Artist, existingMeta.Album, existingMeta.Date)
- }
- }
-
- metadata := Metadata{
- Title: actualTitle,
- Artist: actualArtist,
- Album: actualAlbum,
- AlbumArtist: req.AlbumArtist,
- Date: actualDate,
- TrackNumber: actualTrackNum,
- TotalTracks: req.TotalTracks,
- DiscNumber: actualDiscNum,
- ISRC: req.ISRC,
- Genre: req.Genre,
- Label: req.Label,
- Copyright: req.Copyright,
- }
-
- var coverData []byte
- if parallelResult != nil && parallelResult.CoverData != nil && len(parallelResult.CoverData) > 0 {
- coverData = parallelResult.CoverData
- GoLog("[Amazon] Using parallel-fetched cover (%d bytes)\n", len(coverData))
- } else {
- existingCover, coverErr := ExtractCoverArt(actualOutputPath)
- if coverErr == nil && len(existingCover) > 0 {
- coverData = existingCover
- GoLog("[Amazon] Using existing cover from Amazon file (%d bytes)\n", len(coverData))
- } else {
- GoLog("[Amazon] No cover available (parallel fetch failed and no existing cover)\n")
- }
- }
-
- if isSafOutput || needsDecryption || !req.EmbedMetadata {
- if !req.EmbedMetadata {
- GoLog("[Amazon] Metadata embedding disabled by settings, skipping in-backend metadata/lyrics embedding\n")
- } else {
- GoLog("[Amazon] SAF output detected - skipping in-backend metadata/lyrics embedding (handled in Flutter)\n")
- }
- } else {
- isFlacOutput := strings.HasSuffix(strings.ToLower(actualOutputPath), ".flac")
- if isFlacOutput {
- if err := EmbedMetadataWithCoverData(actualOutputPath, metadata, coverData); err != nil {
- GoLog("[Amazon] Warning: failed to embed metadata: %v\n", err)
- }
- } else {
- GoLog("[Amazon] Non-FLAC output detected (%s), skipping native metadata embedding\n", filepath.Ext(actualOutputPath))
- }
-
- if req.EmbedLyrics && parallelResult != nil && parallelResult.LyricsLRC != "" {
- lyricsMode := req.LyricsMode
- if lyricsMode == "" {
- lyricsMode = "embed"
- }
-
- if lyricsMode == "external" || lyricsMode == "both" {
- GoLog("[Amazon] Saving external LRC file...\n")
- if lrcPath, lrcErr := SaveLRCFile(actualOutputPath, parallelResult.LyricsLRC); lrcErr != nil {
- GoLog("[Amazon] Warning: failed to save LRC file: %v\n", lrcErr)
- } else {
- GoLog("[Amazon] LRC file saved: %s\n", lrcPath)
- }
- }
-
- if (lyricsMode == "embed" || lyricsMode == "both") && isFlacOutput {
- GoLog("[Amazon] Embedding parallel-fetched lyrics (%d lines)...\n", len(parallelResult.LyricsData.Lines))
- if embedErr := EmbedLyrics(actualOutputPath, parallelResult.LyricsLRC); embedErr != nil {
- GoLog("[Amazon] Warning: failed to embed lyrics: %v\n", embedErr)
- } else {
- GoLog("[Amazon] Lyrics embedded successfully\n")
- }
- } else if (lyricsMode == "embed" || lyricsMode == "both") && !isFlacOutput {
- GoLog("[Amazon] Skipping embedded lyrics for non-FLAC output\n")
- }
- } else if req.EmbedLyrics {
- GoLog("[Amazon] No lyrics available from parallel fetch\n")
- }
- }
-
- GoLog("[Amazon] Downloaded successfully from Amazon Music\n")
-
- quality := AudioQuality{}
- if isSafOutput || needsDecryption {
- GoLog("[Amazon] SAF output detected - skipping post-write file inspection in backend\n")
- } else {
- quality, err = GetAudioQuality(actualOutputPath)
- if err != nil {
- GoLog("[Amazon] Warning: couldn't read quality from file: %v\n", err)
- } else {
- GoLog("[Amazon] Actual quality: %d-bit/%dHz\n", quality.BitDepth, quality.SampleRate)
- }
-
- finalMeta, metaReadErr := ReadMetadata(actualOutputPath)
- if metaReadErr == nil && finalMeta != nil {
- GoLog("[Amazon] Final metadata from file - Track: %d, Disc: %d, Date: %s\n",
- finalMeta.TrackNumber, finalMeta.DiscNumber, finalMeta.Date)
- actualTrackNum = finalMeta.TrackNumber
- actualDiscNum = finalMeta.DiscNumber
- if finalMeta.Date != "" {
- req.ReleaseDate = finalMeta.Date
- }
- }
- }
-
- // Add to ISRC index for fast duplicate checking.
- // When decryption is pending in Flutter, postpone indexing until final file is settled.
- if !isSafOutput && !needsDecryption {
- AddToISRCIndex(req.OutputDir, req.ISRC, actualOutputPath)
- }
-
- bitDepth := 0
- sampleRate := 0
- if err == nil {
- bitDepth = quality.BitDepth
- sampleRate = quality.SampleRate
- }
-
- lyricsLRC := ""
- if req.EmbedMetadata && req.EmbedLyrics && parallelResult != nil && parallelResult.LyricsLRC != "" {
- lyricsLRC = parallelResult.LyricsLRC
- }
-
- return AmazonDownloadResult{
- FilePath: outputPath,
- BitDepth: bitDepth,
- SampleRate: sampleRate,
- Title: req.TrackName,
- Artist: req.ArtistName,
- Album: req.AlbumName,
- ReleaseDate: req.ReleaseDate,
- TrackNumber: actualTrackNum,
- DiscNumber: actualDiscNum,
- ISRC: req.ISRC,
- LyricsLRC: lyricsLRC,
- DecryptionKey: decryptionKey,
- }, nil
-}
diff --git a/go_backend/amazon_asin_test.go b/go_backend/amazon_asin_test.go
deleted file mode 100644
index 705e9c01..00000000
--- a/go_backend/amazon_asin_test.go
+++ /dev/null
@@ -1,46 +0,0 @@
-package gobackend
-
-import "testing"
-
-func TestExtractAmazonASIN(t *testing.T) {
- tests := []struct {
- name string
- url string
- want string
- }{
- {
- name: "prefers trackAsin over albumAsin",
- url: "https://music.amazon.com/albums/B0ALBUM123?trackAsin=B0TRACK456&musicTerritory=US",
- want: "B0TRACK456",
- },
- {
- name: "extract from tracks path",
- url: "https://music.amazon.com/tracks/B0CYQHGWZJ?musicTerritory=US",
- want: "B0CYQHGWZJ",
- },
- {
- name: "extract from plain query asin",
- url: "https://example.com/?asin=B0CYQHGWZJ",
- want: "B0CYQHGWZJ",
- },
- {
- name: "fallback regex",
- url: "https://example.com/path/B0CYQHGWZJ",
- want: "B0CYQHGWZJ",
- },
- {
- name: "invalid url",
- url: "https://music.amazon.com/tracks/not-valid",
- want: "",
- },
- }
-
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- got := extractAmazonASIN(tt.url)
- if got != tt.want {
- t.Fatalf("extractAmazonASIN() = %q, want %q", got, tt.want)
- }
- })
- }
-}
diff --git a/go_backend/audio_metadata.go b/go_backend/audio_metadata.go
index 4ccfc627..faf4ccf2 100644
--- a/go_backend/audio_metadata.go
+++ b/go_backend/audio_metadata.go
@@ -12,7 +12,6 @@ import (
"strings"
)
-// AudioMetadata represents common audio file metadata
type AudioMetadata struct {
Title string
Artist string
@@ -31,7 +30,6 @@ type AudioMetadata struct {
Comment string
}
-// MP3Quality represents MP3 specific quality info
type MP3Quality struct {
SampleRate int
BitDepth int
@@ -39,7 +37,6 @@ type MP3Quality struct {
Bitrate int
}
-// OggQuality represents Ogg/Opus specific quality info
type OggQuality struct {
SampleRate int
BitDepth int
@@ -47,10 +44,6 @@ type OggQuality struct {
Bitrate int // estimated bitrate in bps
}
-// =============================================================================
-// ID3 Tag Reading (MP3)
-// =============================================================================
-
func ReadID3Tags(filePath string) (*AudioMetadata, error) {
file, err := os.Open(filePath)
if err != nil {
@@ -1210,10 +1203,6 @@ func readLastOggGranulePosition(file *os.File, fileSize int64) int64 {
return 0
}
-// =============================================================================
-// ID3v1 Genre List
-// =============================================================================
-
var id3v1Genres = []string{
"Blues", "Classic Rock", "Country", "Dance", "Disco", "Funk", "Grunge",
"Hip-Hop", "Jazz", "Metal", "New Age", "Oldies", "Other", "Pop", "R&B",
@@ -1244,10 +1233,6 @@ var id3v1Genres = []string{
"Thrash Metal", "Anime", "J-Pop", "Synthpop",
}
-// =============================================================================
-// Cover Art Extraction
-// =============================================================================
-
func extractMP3CoverArt(filePath string) ([]byte, string, error) {
file, err := os.Open(filePath)
if err != nil {
diff --git a/go_backend/cue_parser.go b/go_backend/cue_parser.go
new file mode 100644
index 00000000..a938be2a
--- /dev/null
+++ b/go_backend/cue_parser.go
@@ -0,0 +1,577 @@
+package gobackend
+
+import (
+ "bufio"
+ "encoding/json"
+ "fmt"
+ "os"
+ "path/filepath"
+ "regexp"
+ "strconv"
+ "strings"
+)
+
+// CueSheet represents a parsed .cue file
+type CueSheet struct {
+ // Album-level metadata
+ Performer string `json:"performer"`
+ Title string `json:"title"`
+ FileName string `json:"file_name"`
+ FileType string `json:"file_type"` // WAVE, FLAC, MP3, AIFF, etc.
+ Genre string `json:"genre,omitempty"`
+ Date string `json:"date,omitempty"`
+ Comment string `json:"comment,omitempty"`
+ Composer string `json:"composer,omitempty"`
+ Tracks []CueTrack `json:"tracks"`
+}
+
+// CueTrack represents a single track in a cue sheet
+type CueTrack struct {
+ Number int `json:"number"`
+ Title string `json:"title"`
+ Performer string `json:"performer"`
+ ISRC string `json:"isrc,omitempty"`
+ Composer string `json:"composer,omitempty"`
+ // Index positions in seconds (fractional)
+ StartTime float64 `json:"start_time"` // INDEX 01 in seconds
+ PreGap float64 `json:"pre_gap"` // INDEX 00 in seconds (or -1 if not present)
+}
+
+// CueSplitInfo represents the information needed to split a CUE+audio file
+type CueSplitInfo struct {
+ CuePath string `json:"cue_path"`
+ AudioPath string `json:"audio_path"`
+ Album string `json:"album"`
+ Artist string `json:"artist"`
+ Genre string `json:"genre,omitempty"`
+ Date string `json:"date,omitempty"`
+ Tracks []CueSplitTrack `json:"tracks"`
+}
+
+// CueSplitTrack has the FFmpeg split parameters for a single track
+type CueSplitTrack struct {
+ Number int `json:"number"`
+ Title string `json:"title"`
+ Artist string `json:"artist"`
+ ISRC string `json:"isrc,omitempty"`
+ Composer string `json:"composer,omitempty"`
+ StartSec float64 `json:"start_sec"`
+ EndSec float64 `json:"end_sec"` // -1 means until end of file
+}
+
+var (
+ reRemCommand = regexp.MustCompile(`^REM\s+(\S+)\s+(.+)$`)
+ reQuoted = regexp.MustCompile(`"([^"]*)"`)
+)
+
+// ParseCueFile parses a .cue file and returns a CueSheet
+func ParseCueFile(cuePath string) (*CueSheet, error) {
+ f, err := os.Open(cuePath)
+ if err != nil {
+ return nil, fmt.Errorf("failed to open cue file: %w", err)
+ }
+ defer f.Close()
+
+ sheet := &CueSheet{}
+ var currentTrack *CueTrack
+
+ scanner := bufio.NewScanner(f)
+ for scanner.Scan() {
+ line := strings.TrimSpace(scanner.Text())
+ if line == "" {
+ continue
+ }
+
+ // Handle BOM at start of file
+ if strings.HasPrefix(line, "\xef\xbb\xbf") {
+ line = strings.TrimPrefix(line, "\xef\xbb\xbf")
+ line = strings.TrimSpace(line)
+ }
+
+ upper := strings.ToUpper(line)
+
+ // REM commands (album-level metadata)
+ if strings.HasPrefix(upper, "REM ") {
+ matches := reRemCommand.FindStringSubmatch(line)
+ if len(matches) == 3 {
+ key := strings.ToUpper(matches[1])
+ value := unquoteCue(matches[2])
+ switch key {
+ case "GENRE":
+ sheet.Genre = value
+ case "DATE":
+ sheet.Date = value
+ case "COMMENT":
+ sheet.Comment = value
+ case "COMPOSER":
+ if currentTrack != nil {
+ currentTrack.Composer = value
+ } else {
+ sheet.Composer = value
+ }
+ }
+ }
+ continue
+ }
+
+ // PERFORMER
+ if strings.HasPrefix(upper, "PERFORMER ") {
+ value := unquoteCue(line[len("PERFORMER "):])
+ if currentTrack != nil {
+ currentTrack.Performer = value
+ } else {
+ sheet.Performer = value
+ }
+ continue
+ }
+
+ // TITLE
+ if strings.HasPrefix(upper, "TITLE ") {
+ value := unquoteCue(line[len("TITLE "):])
+ if currentTrack != nil {
+ currentTrack.Title = value
+ } else {
+ sheet.Title = value
+ }
+ continue
+ }
+
+ // FILE
+ if strings.HasPrefix(upper, "FILE ") {
+ rest := line[len("FILE "):]
+ // Extract filename and type
+ // Format: FILE "filename.flac" WAVE
+ // or: FILE filename.flac WAVE
+ fname, ftype := parseCueFileLine(rest)
+ sheet.FileName = fname
+ sheet.FileType = ftype
+ continue
+ }
+
+ // TRACK
+ if strings.HasPrefix(upper, "TRACK ") {
+ // Save previous track
+ if currentTrack != nil {
+ sheet.Tracks = append(sheet.Tracks, *currentTrack)
+ }
+
+ parts := strings.Fields(line)
+ trackNum := 0
+ if len(parts) >= 2 {
+ trackNum, _ = strconv.Atoi(parts[1])
+ }
+
+ currentTrack = &CueTrack{
+ Number: trackNum,
+ PreGap: -1,
+ }
+ continue
+ }
+
+ // INDEX
+ if strings.HasPrefix(upper, "INDEX ") && currentTrack != nil {
+ parts := strings.Fields(line)
+ if len(parts) >= 3 {
+ indexNum, _ := strconv.Atoi(parts[1])
+ timeSec := parseCueTimestamp(parts[2])
+ switch indexNum {
+ case 0:
+ currentTrack.PreGap = timeSec
+ case 1:
+ currentTrack.StartTime = timeSec
+ }
+ }
+ continue
+ }
+
+ // ISRC
+ if strings.HasPrefix(upper, "ISRC ") && currentTrack != nil {
+ currentTrack.ISRC = strings.TrimSpace(line[len("ISRC "):])
+ continue
+ }
+
+ // SONGWRITER (used as composer sometimes)
+ if strings.HasPrefix(upper, "SONGWRITER ") {
+ value := unquoteCue(line[len("SONGWRITER "):])
+ if currentTrack != nil {
+ currentTrack.Composer = value
+ } else {
+ sheet.Composer = value
+ }
+ continue
+ }
+ }
+
+ // Don't forget the last track
+ if currentTrack != nil {
+ sheet.Tracks = append(sheet.Tracks, *currentTrack)
+ }
+
+ if err := scanner.Err(); err != nil {
+ return nil, fmt.Errorf("error reading cue file: %w", err)
+ }
+
+ if len(sheet.Tracks) == 0 {
+ return nil, fmt.Errorf("no tracks found in cue file")
+ }
+
+ return sheet, nil
+}
+
+// parseCueTimestamp converts MM:SS:FF (frames at 75fps) to seconds
+func parseCueTimestamp(ts string) float64 {
+ parts := strings.Split(ts, ":")
+ if len(parts) != 3 {
+ return 0
+ }
+
+ minutes, _ := strconv.Atoi(parts[0])
+ seconds, _ := strconv.Atoi(parts[1])
+ frames, _ := strconv.Atoi(parts[2])
+
+ return float64(minutes)*60 + float64(seconds) + float64(frames)/75.0
+}
+
+// formatCueTimestamp converts seconds to HH:MM:SS.mmm format for FFmpeg
+func formatCueTimestamp(seconds float64) string {
+ if seconds < 0 {
+ return "0"
+ }
+ hours := int(seconds) / 3600
+ mins := (int(seconds) % 3600) / 60
+ secs := seconds - float64(hours*3600) - float64(mins*60)
+ return fmt.Sprintf("%02d:%02d:%06.3f", hours, mins, secs)
+}
+
+// unquoteCue removes surrounding quotes from a CUE value
+func unquoteCue(s string) string {
+ s = strings.TrimSpace(s)
+ if matches := reQuoted.FindStringSubmatch(s); len(matches) == 2 {
+ return matches[1]
+ }
+ return s
+}
+
+// parseCueFileLine parses the FILE command's filename and type
+func parseCueFileLine(rest string) (string, string) {
+ rest = strings.TrimSpace(rest)
+
+ var filename, ftype string
+
+ if strings.HasPrefix(rest, "\"") {
+ // Quoted filename
+ endQuote := strings.Index(rest[1:], "\"")
+ if endQuote >= 0 {
+ filename = rest[1 : endQuote+1]
+ remaining := strings.TrimSpace(rest[endQuote+2:])
+ ftype = remaining
+ } else {
+ filename = rest
+ }
+ } else {
+ // Unquoted filename - last word is the type
+ parts := strings.Fields(rest)
+ if len(parts) >= 2 {
+ ftype = parts[len(parts)-1]
+ filename = strings.Join(parts[:len(parts)-1], " ")
+ } else if len(parts) == 1 {
+ filename = parts[0]
+ }
+ }
+
+ return filename, strings.TrimSpace(ftype)
+}
+
+// ResolveCueAudioPath finds the actual audio file referenced by a .cue sheet.
+// It checks relative to the cue file's directory.
+func ResolveCueAudioPath(cuePath string, cueFileName string) string {
+ cueDir := filepath.Dir(cuePath)
+
+ // 1. Try the exact filename from the .cue
+ candidate := filepath.Join(cueDir, cueFileName)
+ if _, err := os.Stat(candidate); err == nil {
+ return candidate
+ }
+
+ // 2. Try common case variations
+ baseName := strings.TrimSuffix(cueFileName, filepath.Ext(cueFileName))
+ commonExts := []string{".flac", ".wav", ".ape", ".mp3", ".ogg", ".wv", ".m4a"}
+ for _, ext := range commonExts {
+ candidate = filepath.Join(cueDir, baseName+ext)
+ if _, err := os.Stat(candidate); err == nil {
+ return candidate
+ }
+ // Try uppercase ext
+ candidate = filepath.Join(cueDir, baseName+strings.ToUpper(ext))
+ if _, err := os.Stat(candidate); err == nil {
+ return candidate
+ }
+ }
+
+ // 3. Try to find any audio file with the same base name as the .cue file
+ cueBase := strings.TrimSuffix(filepath.Base(cuePath), filepath.Ext(cuePath))
+ for _, ext := range commonExts {
+ candidate = filepath.Join(cueDir, cueBase+ext)
+ if _, err := os.Stat(candidate); err == nil {
+ return candidate
+ }
+ }
+
+ // 4. If there's only one audio file in the directory, use that
+ entries, err := os.ReadDir(cueDir)
+ if err == nil {
+ audioExts := map[string]bool{
+ ".flac": true, ".wav": true, ".ape": true, ".mp3": true,
+ ".ogg": true, ".wv": true, ".m4a": true, ".aiff": true,
+ }
+ var audioFiles []string
+ for _, entry := range entries {
+ if entry.IsDir() {
+ continue
+ }
+ ext := strings.ToLower(filepath.Ext(entry.Name()))
+ if audioExts[ext] {
+ audioFiles = append(audioFiles, filepath.Join(cueDir, entry.Name()))
+ }
+ }
+ if len(audioFiles) == 1 {
+ return audioFiles[0]
+ }
+ }
+
+ return ""
+}
+
+// BuildCueSplitInfo creates the split information from a parsed CUE sheet.
+// This is returned to the Dart side so FFmpeg can perform the splitting.
+// audioDir, if non-empty, overrides the directory for audio file resolution.
+func BuildCueSplitInfo(cuePath string, sheet *CueSheet, audioDir string) (*CueSplitInfo, error) {
+ resolveDir := cuePath
+ if audioDir != "" {
+ // Create a virtual path in audioDir so ResolveCueAudioPath looks there
+ resolveDir = filepath.Join(audioDir, filepath.Base(cuePath))
+ }
+ audioPath := ResolveCueAudioPath(resolveDir, sheet.FileName)
+ if audioPath == "" {
+ return nil, fmt.Errorf("audio file not found for cue sheet: %s (referenced: %s)", cuePath, sheet.FileName)
+ }
+
+ info := &CueSplitInfo{
+ CuePath: cuePath,
+ AudioPath: audioPath,
+ Album: sheet.Title,
+ Artist: sheet.Performer,
+ Genre: sheet.Genre,
+ Date: sheet.Date,
+ }
+
+ for i, track := range sheet.Tracks {
+ performer := track.Performer
+ if performer == "" {
+ performer = sheet.Performer
+ }
+
+ composer := track.Composer
+ if composer == "" {
+ composer = sheet.Composer
+ }
+
+ // End time is the start of the next track, or -1 for the last track
+ endSec := float64(-1)
+ if i+1 < len(sheet.Tracks) {
+ nextTrack := sheet.Tracks[i+1]
+ // Use pre-gap of next track if available, otherwise its start time
+ if nextTrack.PreGap >= 0 {
+ endSec = nextTrack.PreGap
+ } else {
+ endSec = nextTrack.StartTime
+ }
+ }
+
+ info.Tracks = append(info.Tracks, CueSplitTrack{
+ Number: track.Number,
+ Title: track.Title,
+ Artist: performer,
+ ISRC: track.ISRC,
+ Composer: composer,
+ StartSec: track.StartTime,
+ EndSec: endSec,
+ })
+ }
+
+ return info, nil
+}
+
+// ParseCueFileJSON parses a .cue file and returns JSON with split info.
+// This is the main entry point called from Dart via the platform bridge.
+// audioDir, if non-empty, overrides the directory used for resolving the
+// referenced audio file (useful when the .cue was copied to a temp dir
+// but the audio still lives in the original location, e.g. SAF).
+func ParseCueFileJSON(cuePath string, audioDir string) (string, error) {
+ sheet, err := ParseCueFile(cuePath)
+ if err != nil {
+ return "", fmt.Errorf("failed to parse cue file: %w", err)
+ }
+
+ info, err := BuildCueSplitInfo(cuePath, sheet, audioDir)
+ if err != nil {
+ return "", err
+ }
+
+ jsonBytes, err := json.Marshal(info)
+ if err != nil {
+ return "", fmt.Errorf("failed to marshal cue split info: %w", err)
+ }
+
+ return string(jsonBytes), nil
+}
+
+// ScanCueFileForLibrary parses a .cue file and returns multiple LibraryScanResult
+// entries, one per track. This is used by the library scanner to populate the
+// library with individual track entries from a single CUE+FLAC album.
+func ScanCueFileForLibrary(cuePath string, scanTime string) ([]LibraryScanResult, error) {
+ return scanCueFileForLibraryInternal(cuePath, "", "", 0, scanTime)
+}
+
+// ScanCueFileForLibraryExt is like ScanCueFileForLibrary but with extra parameters
+// for SAF (Storage Access Framework) scenarios:
+// - audioDir: if non-empty, overrides the directory used to find the audio file
+// - virtualPathPrefix: if non-empty, used instead of cuePath as the base for
+// virtual file paths (e.g. a content:// URI). IDs are also based on this.
+// - fileModTime: if > 0, used as the FileModTime for all results instead of
+// stat-ing the cuePath on disk (useful when the real file lives behind SAF)
+func ScanCueFileForLibraryExt(cuePath, audioDir, virtualPathPrefix string, fileModTime int64, scanTime string) ([]LibraryScanResult, error) {
+ return scanCueFileForLibraryInternal(cuePath, audioDir, virtualPathPrefix, fileModTime, scanTime)
+}
+
+func scanCueFileForLibraryInternal(cuePath, audioDir, virtualPathPrefix string, fileModTime int64, scanTime string) ([]LibraryScanResult, error) {
+ sheet, err := ParseCueFile(cuePath)
+ if err != nil {
+ return nil, err
+ }
+
+ // Resolve audio file — optionally in an overridden directory
+ resolveBase := cuePath
+ if audioDir != "" {
+ resolveBase = filepath.Join(audioDir, filepath.Base(cuePath))
+ }
+ audioPath := ResolveCueAudioPath(resolveBase, sheet.FileName)
+ if audioPath == "" {
+ return nil, fmt.Errorf("audio file not found for cue: %s (referenced: %s)", cuePath, sheet.FileName)
+ }
+
+ // Try to get quality info from the audio file
+ var bitDepth, sampleRate int
+ var totalDurationSec float64
+ audioExt := strings.ToLower(filepath.Ext(audioPath))
+ switch audioExt {
+ case ".flac":
+ quality, qErr := GetAudioQuality(audioPath)
+ if qErr == nil {
+ bitDepth = quality.BitDepth
+ sampleRate = quality.SampleRate
+ if quality.SampleRate > 0 && quality.TotalSamples > 0 {
+ totalDurationSec = float64(quality.TotalSamples) / float64(quality.SampleRate)
+ }
+ }
+ case ".mp3":
+ quality, qErr := GetMP3Quality(audioPath)
+ if qErr == nil {
+ sampleRate = quality.SampleRate
+ totalDurationSec = float64(quality.Duration)
+ }
+ }
+
+ // Extract cover from audio file for all tracks
+ var coverPath string
+ libraryCoverCacheMu.RLock()
+ coverCacheDir := libraryCoverCacheDir
+ libraryCoverCacheMu.RUnlock()
+ if coverCacheDir != "" {
+ cp, err := SaveCoverToCache(audioPath, coverCacheDir)
+ if err == nil && cp != "" {
+ coverPath = cp
+ }
+ }
+
+ // Determine the base path for virtual paths and IDs
+ pathBase := cuePath
+ if virtualPathPrefix != "" {
+ pathBase = virtualPathPrefix
+ }
+
+ // Determine fileModTime
+ modTime := fileModTime
+ if modTime <= 0 {
+ if info, err := os.Stat(cuePath); err == nil {
+ modTime = info.ModTime().UnixMilli()
+ }
+ }
+
+ var results []LibraryScanResult
+ for i, track := range sheet.Tracks {
+ performer := track.Performer
+ if performer == "" {
+ performer = sheet.Performer
+ }
+ if performer == "" {
+ performer = "Unknown Artist"
+ }
+
+ title := track.Title
+ if title == "" {
+ title = fmt.Sprintf("Track %02d", track.Number)
+ }
+
+ album := sheet.Title
+ if album == "" {
+ album = "Unknown Album"
+ }
+
+ // Calculate duration for this track
+ var duration int
+ if i+1 < len(sheet.Tracks) {
+ nextStart := sheet.Tracks[i+1].StartTime
+ if sheet.Tracks[i+1].PreGap >= 0 {
+ nextStart = sheet.Tracks[i+1].PreGap
+ }
+ duration = int(nextStart - track.StartTime)
+ } else if totalDurationSec > 0 {
+ duration = int(totalDurationSec - track.StartTime)
+ }
+
+ // Use a unique ID based on pathBase + track number
+ id := generateLibraryID(fmt.Sprintf("%s#track%d", pathBase, track.Number))
+
+ // Use a virtual file path that includes the track number to ensure
+ // uniqueness in the database (file_path has a UNIQUE constraint).
+ // Format: /path/to/album.cue#track01 or content://...album.cue#track01
+ virtualFilePath := fmt.Sprintf("%s#track%02d", pathBase, track.Number)
+
+ result := LibraryScanResult{
+ ID: id,
+ TrackName: title,
+ ArtistName: performer,
+ AlbumName: album,
+ AlbumArtist: sheet.Performer,
+ FilePath: virtualFilePath,
+ CoverPath: coverPath,
+ ScannedAt: scanTime,
+ ISRC: track.ISRC,
+ TrackNumber: track.Number,
+ DiscNumber: 1,
+ Duration: duration,
+ ReleaseDate: sheet.Date,
+ BitDepth: bitDepth,
+ SampleRate: sampleRate,
+ Genre: sheet.Genre,
+ Format: "cue+" + strings.TrimPrefix(audioExt, "."),
+ }
+
+ result.FileModTime = modTime
+
+ results = append(results, result)
+ }
+
+ return results, nil
+}
diff --git a/go_backend/deezer_download.go b/go_backend/deezer_download.go
index 0c64b658..01e5c654 100644
--- a/go_backend/deezer_download.go
+++ b/go_backend/deezer_download.go
@@ -120,7 +120,7 @@ func (c *DeezerClient) DownloadFromYoinkify(spotifyURL, outputPath string, outpu
req.Header.Set("Accept", "*/*")
req.Header.Set("User-Agent", getRandomUserAgent())
- resp, err := c.httpClient.Do(req)
+ resp, err := GetDownloadClient().Do(req)
if err != nil {
if isDownloadCancelled(itemID) {
return ErrDownloadCancelled
@@ -324,7 +324,7 @@ func (c *DeezerClient) DownloadFromMusicDL(deezerTrackURL, outputPath string, ou
}
req.Header.Set("User-Agent", getRandomUserAgent())
- resp, err := c.httpClient.Do(req)
+ resp, err := GetDownloadClient().Do(req)
if err != nil {
if isDownloadCancelled(itemID) {
return ErrDownloadCancelled
diff --git a/go_backend/exports.go b/go_backend/exports.go
index 3c05a0fd..00791180 100644
--- a/go_backend/exports.go
+++ b/go_backend/exports.go
@@ -32,126 +32,6 @@ func ParseSpotifyURL(url string) (string, error) {
return string(jsonBytes), nil
}
-func SetSpotifyAPICredentials(clientID, clientSecret string) {
- SetSpotifyCredentials(clientID, clientSecret)
-}
-
-func CheckSpotifyCredentials() bool {
- return HasSpotifyCredentials()
-}
-
-func GetSpotifyMetadata(spotifyURL string) (string, error) {
- ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
- defer cancel()
-
- client, err := NewSpotifyMetadataClient()
- if err != nil {
- if shouldTrySpotFetchFallback(err) {
- data, apiErr := GetSpotifyDataWithAPI(ctx, spotifyURL, DefaultSpotFetchAPIBaseURL)
- if apiErr == nil {
- jsonBytes, marshalErr := json.Marshal(data)
- if marshalErr != nil {
- return "", marshalErr
- }
- return string(jsonBytes), nil
- }
- }
- return "", err
- }
- data, err := client.GetFilteredData(ctx, spotifyURL, false, 0)
- if err != nil {
- if shouldTrySpotFetchFallback(err) {
- fallbackData, apiErr := GetSpotifyDataWithAPI(ctx, spotifyURL, DefaultSpotFetchAPIBaseURL)
- if apiErr == nil {
- jsonBytes, marshalErr := json.Marshal(fallbackData)
- if marshalErr != nil {
- return "", marshalErr
- }
- return string(jsonBytes), nil
- }
- }
- return "", err
- }
-
- jsonBytes, err := json.Marshal(data)
- if err != nil {
- return "", err
- }
-
- return string(jsonBytes), nil
-}
-
-func SearchSpotify(query string, limit int) (string, error) {
- ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
- defer cancel()
-
- client, err := NewSpotifyMetadataClient()
- if err != nil {
- return "", err
- }
- results, err := client.SearchTracks(ctx, query, limit)
- if err != nil {
- return "", err
- }
-
- jsonBytes, err := json.Marshal(results)
- if err != nil {
- return "", err
- }
-
- return string(jsonBytes), nil
-}
-
-func SearchSpotifyAll(query string, trackLimit, artistLimit int) (string, error) {
- ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
- defer cancel()
-
- client, err := NewSpotifyMetadataClient()
- if err != nil {
- return "", err
- }
- results, err := client.SearchAll(ctx, query, trackLimit, artistLimit)
- if err != nil {
- return "", err
- }
-
- jsonBytes, err := json.Marshal(results)
- if err != nil {
- return "", err
- }
-
- return string(jsonBytes), nil
-}
-
-func GetSpotifyRelatedArtists(artistID string, limit int) (string, error) {
- ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
- defer cancel()
-
- client, err := NewSpotifyMetadataClient()
- if err != nil {
- return "", err
- }
-
- normalizedArtistID := strings.TrimSpace(strings.TrimPrefix(artistID, "spotify:"))
- if normalizedArtistID == "" {
- return "", fmt.Errorf("invalid Spotify artist ID")
- }
-
- artists, err := client.GetRelatedArtists(ctx, normalizedArtistID, limit)
- if err != nil {
- return "", err
- }
-
- resp := map[string]interface{}{
- "artists": artists,
- }
- jsonBytes, err := json.Marshal(resp)
- if err != nil {
- return "", err
- }
- return string(jsonBytes), nil
-}
-
func CheckAvailability(spotifyID, isrc string) (string, error) {
client := NewSongLinkClient()
availability, err := client.CheckTrackAvailability(spotifyID, isrc)
@@ -478,25 +358,6 @@ func DownloadTrack(requestJSON string) (string, error) {
}
}
err = qobuzErr
- case "amazon":
- amazonResult, amazonErr := downloadFromAmazon(req)
- if amazonErr == nil {
- result = DownloadResult{
- FilePath: amazonResult.FilePath,
- BitDepth: amazonResult.BitDepth,
- SampleRate: amazonResult.SampleRate,
- Title: amazonResult.Title,
- Artist: amazonResult.Artist,
- Album: amazonResult.Album,
- ReleaseDate: amazonResult.ReleaseDate,
- TrackNumber: amazonResult.TrackNumber,
- DiscNumber: amazonResult.DiscNumber,
- ISRC: amazonResult.ISRC,
- LyricsLRC: amazonResult.LyricsLRC,
- DecryptionKey: amazonResult.DecryptionKey,
- }
- }
- err = amazonErr
case "deezer":
deezerResult, deezerErr := downloadFromDeezer(req)
if deezerErr == nil {
@@ -640,7 +501,7 @@ func DownloadWithFallback(requestJSON string) (string, error) {
enrichRequestExtendedMetadata(&req)
- allServices := []string{"tidal", "qobuz", "amazon", "deezer"}
+ allServices := []string{"tidal", "qobuz", "deezer"}
preferredService := req.Service
if preferredService == "" {
preferredService = "tidal"
@@ -707,27 +568,6 @@ func DownloadWithFallback(requestJSON string) (string, error) {
GoLog("[DownloadWithFallback] Qobuz error: %v\n", qobuzErr)
}
err = qobuzErr
- case "amazon":
- amazonResult, amazonErr := downloadFromAmazon(req)
- if amazonErr == nil {
- result = DownloadResult{
- FilePath: amazonResult.FilePath,
- BitDepth: amazonResult.BitDepth,
- SampleRate: amazonResult.SampleRate,
- Title: amazonResult.Title,
- Artist: amazonResult.Artist,
- Album: amazonResult.Album,
- ReleaseDate: amazonResult.ReleaseDate,
- TrackNumber: amazonResult.TrackNumber,
- DiscNumber: amazonResult.DiscNumber,
- ISRC: amazonResult.ISRC,
- LyricsLRC: amazonResult.LyricsLRC,
- DecryptionKey: amazonResult.DecryptionKey,
- }
- } else if !errors.Is(amazonErr, ErrDownloadCancelled) {
- GoLog("[DownloadWithFallback] Amazon error: %v\n", amazonErr)
- }
- err = amazonErr
case "deezer":
deezerResult, deezerErr := downloadFromDeezer(req)
if deezerErr == nil {
@@ -824,6 +664,7 @@ func CleanupConnections() {
func ReadFileMetadata(filePath string) (string, error) {
lower := strings.ToLower(filePath)
isFlac := strings.HasSuffix(lower, ".flac")
+ isM4A := strings.HasSuffix(lower, ".m4a") || strings.HasSuffix(lower, ".aac")
isMp3 := strings.HasSuffix(lower, ".mp3")
isOgg := strings.HasSuffix(lower, ".opus") || strings.HasSuffix(lower, ".ogg")
@@ -873,6 +714,12 @@ func ReadFileMetadata(filePath string) (string, error) {
result["duration"] = int(quality.TotalSamples / int64(quality.SampleRate))
}
}
+ } else if isM4A {
+ quality, qualityErr := GetM4AQuality(filePath)
+ if qualityErr == nil {
+ result["bit_depth"] = quality.BitDepth
+ result["sample_rate"] = quality.SampleRate
+ }
} else if isMp3 {
meta, err := ReadID3Tags(filePath)
if err == nil && meta != nil {
@@ -934,6 +781,32 @@ func ReadFileMetadata(filePath string) (string, error) {
return string(jsonBytes), nil
}
+// ParseCueSheet parses a .cue file and returns JSON with split information.
+// This is called from Dart to get track listing and timing data for CUE splitting.
+// audioDir, if non-empty, overrides the directory used for resolving the
+// referenced audio file (useful for SAF temp file scenarios).
+func ParseCueSheet(cuePath string, audioDir string) (string, error) {
+ return ParseCueFileJSON(cuePath, audioDir)
+}
+
+// ScanCueSheetForLibrary parses a .cue file and returns a JSON array of
+// LibraryScanResult entries (one per track). This is the SAF-friendly variant:
+// - audioDir overrides where the referenced audio file is resolved
+// - virtualPathPrefix replaces cuePath in filePath / id fields (e.g. a content:// URI)
+// - fileModTime is stamped on every result (pass 0 to stat cuePath instead)
+func ScanCueSheetForLibrary(cuePath, audioDir, virtualPathPrefix string, fileModTime int64) (string, error) {
+ scanTime := time.Now().UTC().Format(time.RFC3339)
+ results, err := ScanCueFileForLibraryExt(cuePath, audioDir, virtualPathPrefix, fileModTime, scanTime)
+ if err != nil {
+ return "[]", err
+ }
+ jsonBytes, err := json.Marshal(results)
+ if err != nil {
+ return "[]", fmt.Errorf("failed to marshal cue scan results: %w", err)
+ }
+ return string(jsonBytes), nil
+}
+
// EditFileMetadata writes metadata to an audio file.
// For FLAC files, uses native Go FLAC library.
// For MP3/Opus, returns the metadata map so Dart can use FFmpeg.
@@ -1446,28 +1319,6 @@ func GetSpotifyMetadataWithDeezerFallback(spotifyURL string) (string, error) {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
- var spotifyErr error
-
- client, err := NewSpotifyMetadataClient()
- if err != nil {
- LogWarn("Spotify", "Credentials not configured, falling back to Deezer")
- spotifyErr = err
- } else {
- data, err := client.GetFilteredData(ctx, spotifyURL, false, 0)
- if err == nil {
- jsonBytes, err := json.Marshal(data)
- if err != nil {
- return "", err
- }
- return string(jsonBytes), nil
- }
-
- spotifyErr = err
- if !shouldTrySpotFetchFallback(err) {
- return "", err
- }
- }
-
spotFetchData, apiErr := GetSpotifyDataWithAPI(ctx, spotifyURL, DefaultSpotFetchAPIBaseURL)
if apiErr == nil {
GoLog("[Fallback] Spotify metadata fetched via SpotFetch API\n")
@@ -1481,9 +1332,6 @@ func GetSpotifyMetadataWithDeezerFallback(spotifyURL string) (string, error) {
parsed, parseErr := parseSpotifyURI(spotifyURL)
if parseErr != nil {
- if spotifyErr != nil {
- return "", fmt.Errorf("spotify failed (%v), SpotFetch fallback failed (%v), and URL parsing failed: %w", spotifyErr, apiErr, parseErr)
- }
return "", fmt.Errorf("SpotFetch fallback failed (%v) and URL parsing failed: %w", apiErr, parseErr)
}
@@ -1494,15 +1342,9 @@ func GetSpotifyMetadataWithDeezerFallback(spotifyURL string) (string, error) {
}
if parsed.Type == "artist" {
- if spotifyErr != nil {
- return "", fmt.Errorf("spotify metadata unavailable (%v) and SpotFetch fallback failed (%v). Artist pages require Spotify/SpotFetch API", spotifyErr, apiErr)
- }
- return "", fmt.Errorf("SpotFetch fallback failed (%v). Artist pages require Spotify/SpotFetch API", apiErr)
+ return "", fmt.Errorf("SpotFetch fallback failed (%v). Artist pages now require SpotFetch or a metadata extension such as spotify-web", apiErr)
}
- if spotifyErr != nil {
- return "", fmt.Errorf("spotify metadata unavailable (%v), SpotFetch fallback failed (%v), and Deezer conversion is unavailable for playlists", spotifyErr, apiErr)
- }
return "", fmt.Errorf("SpotFetch fallback failed (%v), and Deezer conversion is unavailable for playlists", apiErr)
}
@@ -1579,11 +1421,6 @@ func GetTidalURLFromDeezerTrack(deezerTrackID string) (string, error) {
return client.GetTidalURLFromDeezer(deezerTrackID)
}
-func GetAmazonURLFromDeezerTrack(deezerTrackID string) (string, error) {
- client := NewSongLinkClient()
- return client.GetAmazonURLFromDeezer(deezerTrackID)
-}
-
func errorResponse(msg string) (string, error) {
errorType := "unknown"
lowerMsg := strings.ToLower(msg)
@@ -1838,8 +1675,8 @@ func ReEnrichFile(requestJSON string) (string, error) {
GoLog("[ReEnrich] Starting re-enrichment for: %s\n", req.FilePath)
- // When search_online is true, search for metadata from internet
- // Priority: 1) Deezer (reliable, no credentials) 2) Extension providers (spotify-web etc) 3) Spotify built-in API (last resort, deprecated)
+ // When search_online is true, search for metadata from internet.
+ // Priority: 1) Deezer (reliable, no credentials) 2) Extension providers (spotify-web etc)
if req.SearchOnline && req.TrackName != "" && req.ArtistName != "" {
GoLog("[ReEnrich] Searching online metadata for: %s - %s\n", req.TrackName, req.ArtistName)
searchQuery := req.TrackName + " " + req.ArtistName
@@ -1913,37 +1750,6 @@ func ReEnrichFile(requestJSON string) (string, error) {
}
}
- // 3) Try Spotify built-in API as last resort (will be deprecated)
- if !found {
- GoLog("[ReEnrich] Trying Spotify API (fallback)...\n")
- spotifyClient, spotifyErr := NewSpotifyMetadataClient()
- if spotifyErr == nil {
- ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
- results, err := spotifyClient.SearchTracks(ctx, searchQuery, 5)
- cancel()
- if err == nil && len(results.Tracks) > 0 {
- track := results.Tracks[0]
- GoLog("[ReEnrich] Spotify match: %s - %s (album: %s)\n", track.Name, track.Artists, track.AlbumName)
- req.SpotifyID = track.SpotifyID
- req.AlbumName = track.AlbumName
- req.AlbumArtist = track.AlbumArtist
- req.TrackNumber = track.TrackNumber
- req.DiscNumber = track.DiscNumber
- req.ReleaseDate = track.ReleaseDate
- req.ISRC = track.ISRC
- if track.Images != "" {
- req.CoverURL = track.Images
- }
- req.DurationMs = int64(track.DurationMS)
- found = true
- } else if err != nil {
- GoLog("[ReEnrich] Spotify search failed: %v\n", err)
- }
- } else {
- GoLog("[ReEnrich] Spotify client unavailable: %v\n", spotifyErr)
- }
- }
-
// Try to get extended metadata (genre, label) from Deezer if not already set
if found && req.ISRC != "" && (req.Genre == "" || req.Label == "") {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
@@ -2146,8 +1952,6 @@ func ReEnrichFile(requestJSON string) (string, error) {
return string(jsonBytes), nil
}
-// ==================== EXTENSION SYSTEM ====================
-
func InitExtensionSystem(extensionsDir, dataDir string) error {
manager := GetExtensionManager()
if err := manager.SetDirectories(extensionsDir, dataDir); err != nil {
@@ -2519,8 +2323,6 @@ func GetAllPendingFFmpegCommandsJSON() (string, error) {
return string(jsonBytes), nil
}
-// ==================== EXTENSION CUSTOM SEARCH ====================
-
func EnrichTrackWithExtensionJSON(extensionID, trackJSON string) (string, error) {
manager := GetExtensionManager()
ext, err := manager.GetExtension(extensionID)
@@ -3273,9 +3075,6 @@ func GetExtensionBrowseCategoriesJSON(extensionID string) (string, error) {
return callExtensionFunctionJSON(extensionID, "getBrowseCategories", 30*time.Second)
}
-// ==================== LOCAL LIBRARY SCANNING ====================
-
-// SetLibraryCoverCacheDirJSON sets the directory for caching extracted cover art
func SetLibraryCoverCacheDirJSON(cacheDir string) {
SetLibraryCoverCacheDir(cacheDir)
}
@@ -3284,9 +3083,6 @@ func ScanLibraryFolderJSON(folderPath string) (string, error) {
return ScanLibraryFolder(folderPath)
}
-// ScanLibraryFolderIncrementalJSON performs an incremental library scan
-// existingFilesJSON: JSON object mapping filePath -> modTime (unix millis)
-// Returns IncrementalScanResult as JSON
func ScanLibraryFolderIncrementalJSON(folderPath, existingFilesJSON string) (string, error) {
return ScanLibraryFolderIncremental(folderPath, existingFilesJSON)
}
diff --git a/go_backend/extension_manager.go b/go_backend/extension_manager.go
index b9b460c2..5c73e251 100644
--- a/go_backend/extension_manager.go
+++ b/go_backend/extension_manager.go
@@ -401,7 +401,6 @@ func (m *ExtensionManager) loadExtensionFromDirectory(dirPath string) (*LoadedEx
return nil, fmt.Errorf("failed to read manifest.json: %w", err)
}
- // Parse and validate manifest
manifest, err := ParseManifest(manifestData)
if err != nil {
return nil, fmt.Errorf("Invalid extension manifest: %w", err)
@@ -467,17 +466,11 @@ func (m *ExtensionManager) RemoveExtension(extensionID string) error {
}
}
- // Optionally remove data directory (keep for now to preserve settings)
- // if ext.DataDir != "" {
- // os.RemoveAll(ext.DataDir)
- // }
-
return nil
}
// Only allows upgrades (new version > current version), not downgrades
func (m *ExtensionManager) UpgradeExtension(filePath string) (*LoadedExtension, error) {
- // Validate file extension
if !strings.HasSuffix(strings.ToLower(filePath), ".spotiflac-ext") {
return nil, fmt.Errorf("Invalid file format. Please select a .spotiflac-ext file")
}
@@ -529,7 +522,6 @@ func (m *ExtensionManager) UpgradeExtension(filePath string) (*LoadedExtension,
return nil, fmt.Errorf("Extension '%s' is not installed. Use install instead of upgrade.", newManifest.DisplayName)
}
- // Compare versions - only allow upgrade, not downgrade
versionCompare := compareVersions(newManifest.Version, existing.Manifest.Version)
if versionCompare < 0 {
return nil, fmt.Errorf("Cannot downgrade extension. Current version: %s, New version: %s", existing.Manifest.Version, newManifest.Version)
@@ -540,7 +532,6 @@ func (m *ExtensionManager) UpgradeExtension(filePath string) (*LoadedExtension,
GoLog("[Extension] Upgrading %s from v%s to v%s\n", newManifest.DisplayName, existing.Manifest.Version, newManifest.Version)
- // Save data directory path and enabled state (we want to preserve them)
extDataDir := existing.DataDir
extDir := existing.SourceDir
wasEnabled := existing.Enabled
@@ -601,7 +592,6 @@ func (m *ExtensionManager) UpgradeExtension(filePath string) (*LoadedExtension,
SourceDir: extDir,
}
- // Initialize Goja VM
if err := m.initializeVM(ext); err != nil {
ext.Error = err.Error()
ext.Enabled = false
@@ -626,7 +616,6 @@ type ExtensionUpgradeInfo struct {
}
func (m *ExtensionManager) checkExtensionUpgradeInternal(filePath string) (*ExtensionUpgradeInfo, error) {
- // Validate file extension
if !strings.HasSuffix(strings.ToLower(filePath), ".spotiflac-ext") {
return nil, fmt.Errorf("Invalid file format. Please select a .spotiflac-ext file")
}
@@ -675,7 +664,6 @@ func (m *ExtensionManager) checkExtensionUpgradeInternal(filePath string) (*Exte
}
if !exists {
- // Not installed - this is a new install, not upgrade
info.CurrentVersion = ""
info.CanUpgrade = false
} else {
@@ -739,7 +727,6 @@ func (m *ExtensionManager) GetInstalledExtensionsJSON() (string, error) {
permissions = append(permissions, "storage:enabled")
}
- // Determine status
status := "loaded"
if ext.Error != "" {
status = "error"
@@ -940,7 +927,6 @@ func (m *ExtensionManager) InvokeAction(extensionID string, actionName string) (
return nil, fmt.Errorf("extension is disabled")
}
- // Call the action function on the extension object
script := fmt.Sprintf(`
(function() {
if (typeof extension !== 'undefined' && typeof extension.%s === 'function') {
diff --git a/go_backend/extension_manifest.go b/go_backend/extension_manifest.go
index f4164b4f..6166a667 100644
--- a/go_backend/extension_manifest.go
+++ b/go_backend/extension_manifest.go
@@ -1,4 +1,3 @@
-// Package gobackend provides extension manifest parsing and validation
package gobackend
import (
diff --git a/go_backend/extension_providers.go b/go_backend/extension_providers.go
index ceac1af4..89cf02a2 100644
--- a/go_backend/extension_providers.go
+++ b/go_backend/extension_providers.go
@@ -5,6 +5,7 @@ import (
"encoding/json"
"errors"
"fmt"
+ "os"
"path/filepath"
"sort"
"strings"
@@ -99,15 +100,16 @@ type ExtDownloadResult struct {
ErrorMessage string `json:"error_message,omitempty"`
ErrorType string `json:"error_type,omitempty"`
- Title string `json:"title,omitempty"`
- Artist string `json:"artist,omitempty"`
- Album string `json:"album,omitempty"`
- AlbumArtist string `json:"album_artist,omitempty"`
- TrackNumber int `json:"track_number,omitempty"`
- DiscNumber int `json:"disc_number,omitempty"`
- ReleaseDate string `json:"release_date,omitempty"`
- CoverURL string `json:"cover_url,omitempty"`
- ISRC string `json:"isrc,omitempty"`
+ Title string `json:"title,omitempty"`
+ Artist string `json:"artist,omitempty"`
+ Album string `json:"album,omitempty"`
+ AlbumArtist string `json:"album_artist,omitempty"`
+ TrackNumber int `json:"track_number,omitempty"`
+ DiscNumber int `json:"disc_number,omitempty"`
+ ReleaseDate string `json:"release_date,omitempty"`
+ CoverURL string `json:"cover_url,omitempty"`
+ ISRC string `json:"isrc,omitempty"`
+ DecryptionKey string `json:"decryption_key,omitempty"`
}
type ExtensionProviderWrapper struct {
@@ -388,7 +390,7 @@ func (p *ExtensionProviderWrapper) EnrichTrack(track *ExtTrackMetadata) (*ExtTra
return &enrichedTrack, nil
}
-func (p *ExtensionProviderWrapper) CheckAvailability(isrc, trackName, artistName string) (*ExtAvailabilityResult, error) {
+func (p *ExtensionProviderWrapper) CheckAvailability(isrc, trackName, artistName, spotifyID, deezerID string) (*ExtAvailabilityResult, error) {
if !p.extension.Manifest.IsDownloadProvider() {
return nil, fmt.Errorf("extension '%s' is not a download provider", p.extension.ID)
}
@@ -403,11 +405,11 @@ func (p *ExtensionProviderWrapper) CheckAvailability(isrc, trackName, artistName
script := fmt.Sprintf(`
(function() {
if (typeof extension !== 'undefined' && typeof extension.checkAvailability === 'function') {
- return extension.checkAvailability(%q, %q, %q);
+ return extension.checkAvailability(%q, %q, %q, {spotify_id: %q, deezer_id: %q});
}
return null;
})()
- `, isrc, trackName, artistName)
+ `, isrc, trackName, artistName, spotifyID, deezerID)
result, err := RunWithTimeoutAndRecover(p.vm, script, DefaultJSTimeout)
if err != nil {
@@ -631,7 +633,7 @@ func GetProviderPriority() []string {
defer providerPriorityMu.RUnlock()
if len(providerPriority) == 0 {
- return []string{"tidal", "qobuz", "amazon", "deezer"}
+ return []string{"tidal", "qobuz", "deezer"}
}
result := make([]string, len(providerPriority))
@@ -642,8 +644,26 @@ func GetProviderPriority() []string {
func SetMetadataProviderPriority(providerIDs []string) {
metadataProviderPriorityMu.Lock()
defer metadataProviderPriorityMu.Unlock()
- metadataProviderPriority = providerIDs
- GoLog("[Extension] Metadata provider priority set: %v\n", providerIDs)
+
+ sanitized := make([]string, 0, len(providerIDs)+1)
+ seen := map[string]struct{}{}
+ for _, providerID := range providerIDs {
+ providerID = strings.TrimSpace(providerID)
+ if providerID == "" || providerID == "spotify" {
+ continue
+ }
+ if _, exists := seen[providerID]; exists {
+ continue
+ }
+ seen[providerID] = struct{}{}
+ sanitized = append(sanitized, providerID)
+ }
+ if _, exists := seen["deezer"]; !exists {
+ sanitized = append([]string{"deezer"}, sanitized...)
+ }
+
+ metadataProviderPriority = sanitized
+ GoLog("[Extension] Metadata provider priority set: %v\n", sanitized)
}
func GetMetadataProviderPriority() []string {
@@ -651,7 +671,7 @@ func GetMetadataProviderPriority() []string {
defer metadataProviderPriorityMu.RUnlock()
if len(metadataProviderPriority) == 0 {
- return []string{"deezer", "spotify"}
+ return []string{"deezer"}
}
result := make([]string, len(metadataProviderPriority))
@@ -661,7 +681,7 @@ func GetMetadataProviderPriority() []string {
func isBuiltInProvider(providerID string) bool {
switch providerID {
- case "tidal", "qobuz", "amazon", "deezer":
+ case "tidal", "qobuz", "deezer":
return true
default:
return false
@@ -694,6 +714,27 @@ func DownloadWithExtensionFallback(req DownloadRequest) (*DownloadResponse, erro
}
priority = newPriority
GoLog("[DownloadWithExtensionFallback] New priority order: %v\n", priority)
+ } else if !strictMode && req.Service != "" && !isBuiltInProvider(strings.ToLower(req.Service)) {
+ found := false
+ for _, p := range priority {
+ if strings.EqualFold(p, req.Service) {
+ found = true
+ break
+ }
+ }
+ newPriority := []string{req.Service}
+ for _, p := range priority {
+ if !strings.EqualFold(p, req.Service) {
+ newPriority = append(newPriority, p)
+ }
+ }
+ priority = newPriority
+ if !found {
+ GoLog("[DownloadWithExtensionFallback] Extension service '%s' added to priority front\n", req.Service)
+ } else {
+ GoLog("[DownloadWithExtensionFallback] Extension service '%s' moved to priority front\n", req.Service)
+ }
+ GoLog("[DownloadWithExtensionFallback] New priority order: %v\n", priority)
}
var lastErr error
@@ -777,7 +818,7 @@ func DownloadWithExtensionFallback(req DownloadRequest) (*DownloadResponse, erro
GoLog("[DownloadWithExtensionFallback] Downloading from source extension with trackID: %s (skipBuiltInFallback: %v)\n", trackID, skipBuiltIn)
- outputPath := buildOutputPath(req)
+ outputPath := buildOutputPathForExtension(req, ext)
if req.ItemID != "" {
StartItemProgress(req.ItemID)
}
@@ -813,6 +854,7 @@ func DownloadWithExtensionFallback(req DownloadRequest) (*DownloadResponse, erro
Genre: req.Genre,
Label: req.Label,
Copyright: req.Copyright,
+ DecryptionKey: result.DecryptionKey,
}
if req.EmbedMetadata && (req.Genre != "" || req.Label != "") {
@@ -966,7 +1008,7 @@ func DownloadWithExtensionFallback(req DownloadRequest) (*DownloadResponse, erro
provider := NewExtensionProviderWrapper(ext)
- availability, err := provider.CheckAvailability(req.ISRC, req.TrackName, req.ArtistName)
+ availability, err := provider.CheckAvailability(req.ISRC, req.TrackName, req.ArtistName, req.SpotifyID, req.DeezerID)
if err != nil || !availability.Available {
GoLog("[DownloadWithExtensionFallback] %s: not available\n", providerID)
if err != nil {
@@ -975,7 +1017,7 @@ func DownloadWithExtensionFallback(req DownloadRequest) (*DownloadResponse, erro
continue
}
- outputPath := buildOutputPath(req)
+ outputPath := buildOutputPathForExtension(req, ext)
if req.ItemID != "" {
StartItemProgress(req.ItemID)
}
@@ -1011,6 +1053,7 @@ func DownloadWithExtensionFallback(req DownloadRequest) (*DownloadResponse, erro
Genre: req.Genre,
Label: req.Label,
Copyright: req.Copyright,
+ DecryptionKey: result.DecryptionKey,
}
if req.EmbedMetadata && (req.Genre != "" || req.Label != "") {
@@ -1128,25 +1171,6 @@ func tryBuiltInProvider(providerID string, req DownloadRequest) (*DownloadRespon
}
}
err = qobuzErr
- case "amazon":
- amazonResult, amazonErr := downloadFromAmazon(req)
- if amazonErr == nil {
- result = DownloadResult{
- FilePath: amazonResult.FilePath,
- BitDepth: amazonResult.BitDepth,
- SampleRate: amazonResult.SampleRate,
- Title: amazonResult.Title,
- Artist: amazonResult.Artist,
- Album: amazonResult.Album,
- ReleaseDate: amazonResult.ReleaseDate,
- TrackNumber: amazonResult.TrackNumber,
- DiscNumber: amazonResult.DiscNumber,
- ISRC: amazonResult.ISRC,
- LyricsLRC: amazonResult.LyricsLRC,
- DecryptionKey: amazonResult.DecryptionKey,
- }
- }
- err = amazonErr
case "deezer":
deezerResult, deezerErr := downloadFromDeezer(req)
if deezerErr == nil {
@@ -1226,7 +1250,58 @@ func buildOutputPath(req DownloadRequest) string {
ext = "." + ext
}
- return fmt.Sprintf("%s/%s%s", req.OutputDir, filename, ext)
+ outputDir := req.OutputDir
+ if strings.TrimSpace(outputDir) == "" {
+ outputDir = filepath.Join(os.TempDir(), "spotiflac-downloads")
+ os.MkdirAll(outputDir, 0755)
+ AddAllowedDownloadDir(outputDir)
+ }
+
+ return filepath.Join(outputDir, filename+ext)
+}
+
+func buildOutputPathForExtension(req DownloadRequest, ext *LoadedExtension) string {
+ if strings.TrimSpace(req.OutputPath) != "" {
+ return strings.TrimSpace(req.OutputPath)
+ }
+
+ if strings.TrimSpace(req.OutputDir) != "" {
+ return buildOutputPath(req)
+ }
+
+ // SAF mode: use extension's data dir as writable temp location
+ tempDir := filepath.Join(ext.DataDir, "downloads")
+ os.MkdirAll(tempDir, 0755)
+ AddAllowedDownloadDir(tempDir)
+
+ metadata := map[string]interface{}{
+ "title": req.TrackName,
+ "artist": req.ArtistName,
+ "album": req.AlbumName,
+ "album_artist": req.AlbumArtist,
+ "track": req.TrackNumber,
+ "track_number": req.TrackNumber,
+ "disc": req.DiscNumber,
+ "disc_number": req.DiscNumber,
+ "year": extractYear(req.ReleaseDate),
+ "date": req.ReleaseDate,
+ "release_date": req.ReleaseDate,
+ "isrc": req.ISRC,
+ }
+
+ filename := buildFilenameFromTemplate(req.FilenameFormat, metadata)
+ if filename == "" {
+ filename = sanitizeFilename(fmt.Sprintf("%s - %s", req.ArtistName, req.TrackName))
+ }
+
+ outputExt := strings.TrimSpace(req.OutputExt)
+ if outputExt == "" {
+ outputExt = ".flac"
+ } else if !strings.HasPrefix(outputExt, ".") {
+ outputExt = "." + outputExt
+ }
+
+ return filepath.Join(tempDir, filename+outputExt)
}
func (p *ExtensionProviderWrapper) CustomSearch(query string, options map[string]interface{}) ([]ExtTrackMetadata, error) {
@@ -1653,7 +1728,6 @@ func (m *ExtensionManager) HandleURLWithExtension(url string) (*ExtURLHandleResu
}, nil
}
-// GetPostProcessingProviders returns all extensions that provide post-processing
func (m *ExtensionManager) GetPostProcessingProviders() []*ExtensionProviderWrapper {
m.mu.RLock()
defer m.mu.RUnlock()
@@ -1667,7 +1741,6 @@ func (m *ExtensionManager) GetPostProcessingProviders() []*ExtensionProviderWrap
return providers
}
-// RunPostProcessing runs all enabled post-processing hooks on a file
func (m *ExtensionManager) RunPostProcessing(filePath string, metadata map[string]interface{}) (*PostProcessResult, error) {
providers := m.GetPostProcessingProviders()
if len(providers) == 0 {
@@ -1713,7 +1786,6 @@ func (m *ExtensionManager) RunPostProcessing(filePath string, metadata map[strin
return &PostProcessResult{Success: true, NewFilePath: currentPath}, nil
}
-// RunPostProcessingV2 runs all enabled post-processing hooks on a file input.
func (m *ExtensionManager) RunPostProcessingV2(input PostProcessInput, metadata map[string]interface{}) (*PostProcessResult, error) {
providers := m.GetPostProcessingProviders()
if len(providers) == 0 {
@@ -1768,9 +1840,6 @@ func (m *ExtensionManager) RunPostProcessingV2(input PostProcessInput, metadata
return &PostProcessResult{Success: true, NewFilePath: currentInput.Path, NewFileURI: currentInput.URI}, nil
}
-// ==================== Lyrics Provider ====================
-
-// ExtLyricsResult represents lyrics data returned from an extension
type ExtLyricsResult struct {
Lines []ExtLyricsLine `json:"lines"`
SyncType string `json:"syncType"`
@@ -1785,7 +1854,6 @@ type ExtLyricsLine struct {
EndTimeMs int64 `json:"endTimeMs"`
}
-// FetchLyrics calls the extension's fetchLyrics function
func (p *ExtensionProviderWrapper) FetchLyrics(trackName, artistName, albumName string, durationSec float64) (*LyricsResponse, error) {
if !p.extension.Manifest.IsLyricsProvider() {
return nil, fmt.Errorf("extension '%s' is not a lyrics provider", p.extension.ID)
@@ -1885,7 +1953,6 @@ func (p *ExtensionProviderWrapper) FetchLyrics(trackName, artistName, albumName
return response, nil
}
-// GetLyricsProviders returns all enabled extensions that provide lyrics
func (m *ExtensionManager) GetLyricsProviders() []*ExtensionProviderWrapper {
m.mu.RLock()
defer m.mu.RUnlock()
diff --git a/go_backend/extension_runtime_auth.go b/go_backend/extension_runtime_auth.go
index de4ed06c..e17e0d4d 100644
--- a/go_backend/extension_runtime_auth.go
+++ b/go_backend/extension_runtime_auth.go
@@ -1,4 +1,3 @@
-// Package gobackend provides Auth API and PKCE support for extension runtime
package gobackend
import (
@@ -16,8 +15,6 @@ import (
"github.com/dop251/goja"
)
-// ==================== Auth API (OAuth Support) ====================
-
func validateExtensionAuthURL(urlStr string) error {
parsed, err := url.Parse(urlStr)
if err != nil {
@@ -204,9 +201,6 @@ func (r *ExtensionRuntime) authGetTokens(call goja.FunctionCall) goja.Value {
return r.vm.ToValue(result)
}
-// ==================== PKCE Support ====================
-
-// generatePKCEVerifier generates a cryptographically random code verifier
// Length should be between 43-128 characters (RFC 7636)
func generatePKCEVerifier(length int) (string, error) {
if length < 43 {
@@ -394,9 +388,7 @@ func (r *ExtensionRuntime) authStartOAuthWithPKCE(call goja.FunctionCall) goja.V
})
}
-// authExchangeCodeWithPKCE exchanges auth code for tokens using PKCE
// config: { tokenUrl, clientId, redirectUri, code, extraParams }
-// Uses the stored PKCE verifier automatically
func (r *ExtensionRuntime) authExchangeCodeWithPKCE(call goja.FunctionCall) goja.Value {
if len(call.Arguments) < 1 {
return r.vm.ToValue(map[string]interface{}{
@@ -414,7 +406,6 @@ func (r *ExtensionRuntime) authExchangeCodeWithPKCE(call goja.FunctionCall) goja
})
}
- // Required fields
tokenURL, _ := config["tokenUrl"].(string)
clientID, _ := config["clientId"].(string)
redirectURI, _ := config["redirectUri"].(string)
diff --git a/go_backend/extension_runtime_ffmpeg.go b/go_backend/extension_runtime_ffmpeg.go
index 19e1b67c..7c1e8d69 100644
--- a/go_backend/extension_runtime_ffmpeg.go
+++ b/go_backend/extension_runtime_ffmpeg.go
@@ -1,4 +1,3 @@
-// Package gobackend provides FFmpeg API for extension runtime
package gobackend
import (
@@ -10,9 +9,7 @@ import (
"github.com/dop251/goja"
)
-// ==================== FFmpeg API (Post-Processing) ====================
-
-// FFmpegCommand holds a pending FFmpeg command for Flutter to execute
+// FFmpegCommand holds a pending FFmpeg command for Flutter to execute.
type FFmpegCommand struct {
ExtensionID string
Command string
@@ -24,7 +21,6 @@ type FFmpegCommand struct {
Output string
}
-// Global FFmpeg command queue
var (
ffmpegCommands = make(map[string]*FFmpegCommand)
ffmpegCommandsMu sync.RWMutex
diff --git a/go_backend/extension_runtime_file.go b/go_backend/extension_runtime_file.go
index 9bb1191e..414f174c 100644
--- a/go_backend/extension_runtime_file.go
+++ b/go_backend/extension_runtime_file.go
@@ -1,4 +1,3 @@
-// Package gobackend provides File API for extension runtime
package gobackend
import (
@@ -13,8 +12,6 @@ import (
"github.com/dop251/goja"
)
-// ==================== File API (Sandboxed) ====================
-
var (
allowedDownloadDirs []string
allowedDownloadDirsMu sync.RWMutex
diff --git a/go_backend/extension_runtime_http.go b/go_backend/extension_runtime_http.go
index dcdd32f4..65775832 100644
--- a/go_backend/extension_runtime_http.go
+++ b/go_backend/extension_runtime_http.go
@@ -1,4 +1,3 @@
-// Package gobackend provides HTTP API for extension runtime
package gobackend
import (
@@ -12,8 +11,6 @@ import (
"github.com/dop251/goja"
)
-// ==================== HTTP API (Sandboxed) ====================
-
type HTTPResponse struct {
StatusCode int `json:"statusCode"`
Body string `json:"body"`
diff --git a/go_backend/extension_runtime_matching.go b/go_backend/extension_runtime_matching.go
index 4ad4b0e3..30b61e7b 100644
--- a/go_backend/extension_runtime_matching.go
+++ b/go_backend/extension_runtime_matching.go
@@ -1,4 +1,3 @@
-// Package gobackend provides Track Matching API for extension runtime
package gobackend
import (
@@ -7,8 +6,6 @@ import (
"github.com/dop251/goja"
)
-// ==================== Track Matching API ====================
-
func (r *ExtensionRuntime) matchingCompareStrings(call goja.FunctionCall) goja.Value {
if len(call.Arguments) < 2 {
return r.vm.ToValue(0.0)
diff --git a/go_backend/extension_runtime_polyfills.go b/go_backend/extension_runtime_polyfills.go
index 62892c70..a5334e06 100644
--- a/go_backend/extension_runtime_polyfills.go
+++ b/go_backend/extension_runtime_polyfills.go
@@ -1,4 +1,3 @@
-// Package gobackend provides Browser-like Polyfills for extension runtime
package gobackend
import (
@@ -13,12 +12,10 @@ import (
"github.com/dop251/goja"
)
-// ==================== Browser-like Polyfills ====================
// These polyfills make porting browser/Node.js libraries easier
-// without compromising sandbox security
+// without compromising sandbox security.
-// fetchPolyfill implements browser-compatible fetch() API
-// Returns a Promise-like object with json(), text() methods
+// Returns a Promise-like object with json(), text() methods.
func (r *ExtensionRuntime) fetchPolyfill(call goja.FunctionCall) goja.Value {
if len(call.Arguments) < 1 {
return r.createFetchError("URL is required")
@@ -141,7 +138,6 @@ func (r *ExtensionRuntime) fetchPolyfill(call goja.FunctionCall) goja.Value {
return responseObj
}
-// createFetchError creates a fetch error response
func (r *ExtensionRuntime) createFetchError(message string) goja.Value {
errorObj := r.vm.NewObject()
errorObj.Set("ok", false)
@@ -157,7 +153,6 @@ func (r *ExtensionRuntime) createFetchError(message string) goja.Value {
return errorObj
}
-// atobPolyfill implements browser atob() - decode base64 to string
func (r *ExtensionRuntime) atobPolyfill(call goja.FunctionCall) goja.Value {
if len(call.Arguments) < 1 {
return r.vm.ToValue("")
@@ -174,7 +169,6 @@ func (r *ExtensionRuntime) atobPolyfill(call goja.FunctionCall) goja.Value {
return r.vm.ToValue(string(decoded))
}
-// btoaPolyfill implements browser btoa() - encode string to base64
func (r *ExtensionRuntime) btoaPolyfill(call goja.FunctionCall) goja.Value {
if len(call.Arguments) < 1 {
return r.vm.ToValue("")
@@ -183,7 +177,6 @@ func (r *ExtensionRuntime) btoaPolyfill(call goja.FunctionCall) goja.Value {
return r.vm.ToValue(base64.StdEncoding.EncodeToString([]byte(input)))
}
-// registerTextEncoderDecoder registers TextEncoder and TextDecoder classes
func (r *ExtensionRuntime) registerTextEncoderDecoder(vm *goja.Runtime) {
vm.Set("TextEncoder", func(call goja.ConstructorCall) *goja.Object {
encoder := call.This
@@ -429,9 +422,8 @@ func (r *ExtensionRuntime) registerURLClass(vm *goja.Runtime) {
})
}
-// registerJSONGlobal ensures JSON global is properly set up
+// JSON is already built-in to Goja; this ensures a fallback exists.
func (r *ExtensionRuntime) registerJSONGlobal(vm *goja.Runtime) {
- // JSON is already built-in to Goja, but we can enhance it
jsonScript := `
if (typeof JSON === 'undefined') {
var JSON = {
diff --git a/go_backend/extension_runtime_storage.go b/go_backend/extension_runtime_storage.go
index 06cbdd33..50815b40 100644
--- a/go_backend/extension_runtime_storage.go
+++ b/go_backend/extension_runtime_storage.go
@@ -1,4 +1,3 @@
-// Package gobackend provides Storage and Credentials API for extension runtime
package gobackend
import (
@@ -17,8 +16,6 @@ import (
"github.com/dop251/goja"
)
-// ==================== Storage API ====================
-
const (
defaultStorageFlushDelay = 400 * time.Millisecond
storageFlushRetryDelay = 2 * time.Second
diff --git a/go_backend/extension_runtime_utils.go b/go_backend/extension_runtime_utils.go
index c3a7675e..f91918ff 100644
--- a/go_backend/extension_runtime_utils.go
+++ b/go_backend/extension_runtime_utils.go
@@ -1,4 +1,3 @@
-// Package gobackend provides Utility functions for extension runtime
package gobackend
import (
@@ -17,8 +16,6 @@ import (
"github.com/dop251/goja"
)
-// ==================== Utility Functions ====================
-
func (r *ExtensionRuntime) base64Encode(call goja.FunctionCall) goja.Value {
if len(call.Arguments) < 1 {
return r.vm.ToValue("")
diff --git a/go_backend/extension_settings.go b/go_backend/extension_settings.go
index 9ad0c0c1..e241f6e2 100644
--- a/go_backend/extension_settings.go
+++ b/go_backend/extension_settings.go
@@ -1,4 +1,3 @@
-// Package gobackend provides extension settings storage
package gobackend
import (
diff --git a/go_backend/extension_timeout.go b/go_backend/extension_timeout.go
index e9a5605f..76e51cfa 100644
--- a/go_backend/extension_timeout.go
+++ b/go_backend/extension_timeout.go
@@ -1,4 +1,3 @@
-// Package gobackend provides timeout execution for extension JS code
package gobackend
import (
diff --git a/go_backend/httputil.go b/go_backend/httputil.go
index 08137fa8..b54ec489 100644
--- a/go_backend/httputil.go
+++ b/go_backend/httputil.go
@@ -489,7 +489,6 @@ func IsISPBlocking(err error, requestURL string) *ISPBlockingError {
}
}
- // Check error message patterns for common ISP blocking indicators
blockingPatterns := []struct {
pattern string
reason string
@@ -532,7 +531,6 @@ func CheckAndLogISPBlocking(err error, requestURL string, tag string) bool {
return false
}
-// extractDomain extracts the domain from a URL string
func extractDomain(rawURL string) string {
if rawURL == "" {
return "unknown"
diff --git a/go_backend/httputil_utls.go b/go_backend/httputil_utls.go
index bf5ec9ed..4b09deb7 100644
--- a/go_backend/httputil_utls.go
+++ b/go_backend/httputil_utls.go
@@ -91,7 +91,6 @@ func (t *utlsTransport) getPort(u *url.URL) string {
return "80"
}
-// Cloudflare bypass client using uTLS Chrome fingerprint
var cloudflareBypassTransport = newUTLSTransport()
var cloudflareBypassClient = &http.Client{
@@ -111,7 +110,6 @@ func GetCloudflareBypassClient() *http.Client {
func DoRequestWithCloudflareBypass(req *http.Request) (*http.Response, error) {
req.Header.Set("User-Agent", getRandomUserAgent())
- // Try with standard client first
resp, err := sharedClient.Do(req)
if err == nil {
// Check for Cloudflare challenge page (403 with specific markers)
@@ -138,11 +136,9 @@ func DoRequestWithCloudflareBypass(req *http.Request) (*http.Response, error) {
if isCloudflare {
LogDebug("HTTP", "Cloudflare detected, retrying with Chrome TLS fingerprint...")
- // Clone request for retry
reqCopy := req.Clone(req.Context())
reqCopy.Header.Set("User-Agent", getRandomUserAgent())
- // Retry with uTLS Chrome fingerprint
return cloudflareBypassClient.Do(reqCopy)
}
}
@@ -168,11 +164,9 @@ func DoRequestWithCloudflareBypass(req *http.Request) (*http.Response, error) {
if tlsRelated {
LogDebug("HTTP", "TLS error detected, retrying with Chrome TLS fingerprint: %v", err)
- // Clone request for retry
reqCopy := req.Clone(req.Context())
reqCopy.Header.Set("User-Agent", getRandomUserAgent())
- // Retry with uTLS Chrome fingerprint
return cloudflareBypassClient.Do(reqCopy)
}
diff --git a/go_backend/idhs.go b/go_backend/idhs.go
index 72c8cff3..3b339ed0 100644
--- a/go_backend/idhs.go
+++ b/go_backend/idhs.go
@@ -22,13 +22,11 @@ var (
idhsRateLimiter = NewRateLimiter(8, time.Minute) // 8 req/min (below 10 limit)
)
-// IDHSSearchRequest represents the request body for IDHS API
type IDHSSearchRequest struct {
Link string `json:"link"`
Adapters []string `json:"adapters,omitempty"`
}
-// IDHSSearchResponse represents the response from IDHS API
type IDHSSearchResponse struct {
ID string `json:"id"`
Type string `json:"type"` // song, album, artist, podcast, show
@@ -41,7 +39,6 @@ type IDHSSearchResponse struct {
Links []IDHSLink `json:"links"`
}
-// IDHSLink represents a link to a streaming platform
type IDHSLink struct {
Type string `json:"type"` // spotify, youTube, appleMusic, deezer, soundCloud, tidal
URL string `json:"url"`
@@ -49,7 +46,6 @@ type IDHSLink struct {
NotAvailable bool `json:"notAvailable,omitempty"`
}
-// NewIDHSClient creates a new IDHS client
func NewIDHSClient() *IDHSClient {
idhsClientOnce.Do(func() {
globalIDHSClient = &IDHSClient{
@@ -117,7 +113,6 @@ func (c *IDHSClient) Search(link string, adapters []string) (*IDHSSearchResponse
func (c *IDHSClient) GetAvailabilityFromSpotify(spotifyTrackID string) (*TrackAvailability, error) {
spotifyURL := fmt.Sprintf("https://open.spotify.com/track/%s", spotifyTrackID)
- // Request only the platforms we need
adapters := []string{"tidal", "deezer"}
result, err := c.Search(spotifyURL, adapters)
@@ -151,11 +146,9 @@ func (c *IDHSClient) GetAvailabilityFromSpotify(spotifyTrackID string) (*TrackAv
return availability, nil
}
-// GetAvailabilityFromDeezer checks track availability using IDHS
func (c *IDHSClient) GetAvailabilityFromDeezer(deezerTrackID string) (*TrackAvailability, error) {
deezerURL := fmt.Sprintf("https://www.deezer.com/track/%s", deezerTrackID)
- // Request only the platforms we need
adapters := []string{"spotify", "tidal"}
result, err := c.Search(deezerURL, adapters)
diff --git a/go_backend/library_scan.go b/go_backend/library_scan.go
index 52e91fb8..34d5c718 100644
--- a/go_backend/library_scan.go
+++ b/go_backend/library_scan.go
@@ -10,7 +10,6 @@ import (
"time"
)
-// LibraryScanResult represents metadata from a scanned audio file
type LibraryScanResult struct {
ID string `json:"id"`
TrackName string `json:"trackName"`
@@ -42,7 +41,6 @@ type LibraryScanProgress struct {
IsComplete bool `json:"is_complete"`
}
-// IncrementalScanResult contains results of an incremental library scan
type IncrementalScanResult struct {
Scanned []LibraryScanResult `json:"scanned"` // New or updated files
DeletedPaths []string `json:"deletedPaths"` // Files that no longer exist
@@ -65,6 +63,7 @@ var supportedAudioFormats = map[string]bool{
".mp3": true,
".opus": true,
".ogg": true,
+ ".cue": true,
}
type libraryAudioFileInfo struct {
@@ -168,6 +167,23 @@ func ScanLibraryFolder(folderPath string) (string, error) {
scanTime := time.Now().UTC().Format(time.RFC3339)
errorCount := 0
+ // Track audio files referenced by .cue sheets to avoid duplicates
+ cueReferencedAudioFiles := make(map[string]bool)
+
+ // First pass: scan .cue files to collect referenced audio paths
+ for _, filePath := range audioFiles {
+ ext := strings.ToLower(filepath.Ext(filePath))
+ if ext == ".cue" {
+ sheet, err := ParseCueFile(filePath)
+ if err == nil && sheet.FileName != "" {
+ audioPath := ResolveCueAudioPath(filePath, sheet.FileName)
+ if audioPath != "" {
+ cueReferencedAudioFiles[audioPath] = true
+ }
+ }
+ }
+ }
+
for i, filePath := range audioFiles {
select {
case <-cancelCh:
@@ -181,6 +197,28 @@ func ScanLibraryFolder(folderPath string) (string, error) {
libraryScanProgress.ProgressPct = float64(i+1) / float64(totalFiles) * 100
libraryScanProgressMu.Unlock()
+ ext := strings.ToLower(filepath.Ext(filePath))
+
+ // Handle .cue files: produce multiple track results
+ if ext == ".cue" {
+ cueResults, err := ScanCueFileForLibrary(filePath, scanTime)
+ if err != nil {
+ errorCount++
+ GoLog("[LibraryScan] Error scanning cue %s: %v\n", filePath, err)
+ continue
+ }
+ results = append(results, cueResults...)
+ GoLog("[LibraryScan] CUE sheet %s: %d tracks\n", filepath.Base(filePath), len(cueResults))
+ continue
+ }
+
+ // Skip audio files that are referenced by a .cue sheet
+ // (they will be represented by the cue sheet's track entries instead)
+ if cueReferencedAudioFiles[filePath] {
+ GoLog("[LibraryScan] Skipping %s (referenced by .cue sheet)\n", filepath.Base(filePath))
+ continue
+ }
+
result, err := scanAudioFile(filePath, scanTime)
if err != nil {
errorCount++
@@ -216,7 +254,6 @@ func scanAudioFile(filePath, scanTime string) (*LibraryScanResult, error) {
Format: strings.TrimPrefix(ext, "."),
}
- // Get file modification time
if info, err := os.Stat(filePath); err == nil {
result.FileModTime = info.ModTime().UnixMilli()
}
@@ -466,7 +503,6 @@ func ScanLibraryFolderIncremental(folderPath, existingFilesJSON string) (string,
return "{}", fmt.Errorf("path is not a folder: %s", folderPath)
}
- // Parse existing files map
existingFiles := make(map[string]int64)
if existingFilesJSON != "" && existingFilesJSON != "{}" {
if err := json.Unmarshal([]byte(existingFilesJSON), &existingFiles); err != nil {
@@ -476,12 +512,10 @@ func ScanLibraryFolderIncremental(folderPath, existingFilesJSON string) (string,
GoLog("[LibraryScan] Incremental scan starting, %d existing files in database\n", len(existingFiles))
- // Reset progress
libraryScanProgressMu.Lock()
libraryScanProgress = LibraryScanProgress{}
libraryScanProgressMu.Unlock()
- // Setup cancellation
libraryScanCancelMu.Lock()
if libraryScanCancel != nil {
close(libraryScanCancel)
@@ -490,7 +524,6 @@ func ScanLibraryFolderIncremental(folderPath, existingFilesJSON string) (string,
cancelCh := libraryScanCancel
libraryScanCancelMu.Unlock()
- // Collect all audio files with their mod times
currentFiles, err := collectLibraryAudioFiles(folderPath, cancelCh)
if err != nil {
return "{}", err
@@ -509,24 +542,64 @@ func ScanLibraryFolderIncremental(folderPath, existingFilesJSON string) (string,
var filesToScan []libraryAudioFileInfo
skippedCount := 0
+ // Build a set of existing CUE virtual path base files for incremental matching.
+ // CUE tracks are stored with virtual paths like "/path/album.cue#track01".
+ // We need to match these against the actual .cue file's modTime.
+ cueBaseModTimes := make(map[string]int64) // base cue path -> modTime from disk
+ for _, f := range currentFiles {
+ if strings.ToLower(filepath.Ext(f.path)) == ".cue" {
+ cueBaseModTimes[f.path] = f.modTime
+ }
+ }
+
for _, f := range currentFiles {
existingModTime, exists := existingFiles[f.path]
if !exists {
- // New file
+ // For .cue files, also check if any virtual path entries exist
+ if strings.ToLower(filepath.Ext(f.path)) == ".cue" {
+ hasCueTracks := false
+ for existingPath := range existingFiles {
+ if strings.HasPrefix(existingPath, f.path+"#track") {
+ hasCueTracks = true
+ break
+ }
+ }
+ if hasCueTracks {
+ // CUE file exists in DB via virtual paths; check if modTime changed
+ // Use modTime from any virtual path (they all share the same .cue modTime)
+ for existingPath, modTime := range existingFiles {
+ if strings.HasPrefix(existingPath, f.path+"#track") {
+ if f.modTime == modTime {
+ skippedCount++
+ } else {
+ filesToScan = append(filesToScan, f)
+ }
+ break
+ }
+ }
+ continue
+ }
+ }
filesToScan = append(filesToScan, f)
} else if f.modTime != existingModTime {
- // Modified file
filesToScan = append(filesToScan, f)
} else {
- // Unchanged file - skip
skippedCount++
}
}
- // Find deleted files
var deletedPaths []string
for existingPath := range existingFiles {
- if !currentPathSet[existingPath] {
+ // For CUE virtual paths (e.g. "/path/album.cue#track01"),
+ // check if the base .cue file still exists on disk
+ if idx := strings.LastIndex(existingPath, "#track"); idx > 0 {
+ baseCuePath := existingPath[:idx]
+ if currentPathSet[baseCuePath] {
+ continue // Base .cue file still exists, not deleted
+ }
+ // Base CUE file is gone, mark virtual path as deleted
+ deletedPaths = append(deletedPaths, existingPath)
+ } else if !currentPathSet[existingPath] {
deletedPaths = append(deletedPaths, existingPath)
}
}
@@ -551,11 +624,25 @@ func ScanLibraryFolderIncremental(folderPath, existingFilesJSON string) (string,
return string(jsonBytes), nil
}
- // Scan the files that need scanning
results := make([]LibraryScanResult, 0, len(filesToScan))
scanTime := time.Now().UTC().Format(time.RFC3339)
errorCount := 0
+ // Track audio files referenced by .cue sheets to avoid duplicates (incremental)
+ cueReferencedAudioFilesInc := make(map[string]bool)
+ for _, f := range filesToScan {
+ ext := strings.ToLower(filepath.Ext(f.path))
+ if ext == ".cue" {
+ sheet, err := ParseCueFile(f.path)
+ if err == nil && sheet.FileName != "" {
+ audioPath := ResolveCueAudioPath(f.path, sheet.FileName)
+ if audioPath != "" {
+ cueReferencedAudioFilesInc[audioPath] = true
+ }
+ }
+ }
+ }
+
for i, f := range filesToScan {
select {
case <-cancelCh:
@@ -569,6 +656,25 @@ func ScanLibraryFolderIncremental(folderPath, existingFilesJSON string) (string,
libraryScanProgress.ProgressPct = float64(skippedCount+i+1) / float64(totalFiles) * 100
libraryScanProgressMu.Unlock()
+ ext := strings.ToLower(filepath.Ext(f.path))
+
+ // Handle .cue files: produce multiple track results
+ if ext == ".cue" {
+ cueResults, err := ScanCueFileForLibrary(f.path, scanTime)
+ if err != nil {
+ errorCount++
+ GoLog("[LibraryScan] Error scanning cue %s: %v\n", f.path, err)
+ continue
+ }
+ results = append(results, cueResults...)
+ continue
+ }
+
+ // Skip audio files referenced by .cue sheets
+ if cueReferencedAudioFilesInc[f.path] {
+ continue
+ }
+
result, err := scanAudioFile(f.path, scanTime)
if err != nil {
errorCount++
diff --git a/go_backend/lyrics.go b/go_backend/lyrics.go
index 3ec16555..1a6c4771 100644
--- a/go_backend/lyrics.go
+++ b/go_backend/lyrics.go
@@ -41,7 +41,6 @@ var DefaultLyricsProviders = []string{
LyricsProviderQQMusic,
}
-// Global lyrics provider configuration
var (
lyricsProvidersMu sync.RWMutex
lyricsProviders []string // ordered list of enabled providers
@@ -598,7 +597,6 @@ func (c *LyricsClient) FetchLyricsAllSources(spotifyID, trackName, artistName st
return lyricsHasUsableText(l)
}
- // Try extension lyrics providers first
if len(extensionProviders) > 0 {
for _, provider := range extensionProviders {
GoLog("[Lyrics] Trying extension lyrics provider: %s\n", provider.extension.ID)
@@ -621,7 +619,6 @@ func (c *LyricsClient) FetchLyricsAllSources(spotifyID, trackName, artistName st
return &cachedCopy, nil
}
- // Get configured provider order
providerOrder := GetLyricsProviderOrder()
simplifiedTrack := simplifyTrackName(trackName)
diff --git a/go_backend/lyrics_apple.go b/go_backend/lyrics_apple.go
index 957db6fc..538b0b89 100644
--- a/go_backend/lyrics_apple.go
+++ b/go_backend/lyrics_apple.go
@@ -97,7 +97,6 @@ func (m *appleTokenManager) clearToken() {
m.token = ""
}
-// Apple Music API response models
type appleMusicSearchResponse struct {
Results struct {
Songs *struct {
@@ -239,15 +238,12 @@ func (c *AppleMusicClient) FetchLyricsByID(songID string) (string, error) {
return bodyStr, nil
}
-// formatPaxLyricsToLRC converts a pax proxy response to standard LRC format.
func formatPaxLyricsToLRC(rawJSON string, multiPersonWordByWord bool) (string, error) {
- // Try to parse as PaxResponse first
var paxResp paxResponse
if err := json.Unmarshal([]byte(rawJSON), &paxResp); err == nil && paxResp.Content != nil {
return formatPaxContent(paxResp.Type, paxResp.Content, multiPersonWordByWord), nil
}
- // Try to parse as a direct list of PaxLyrics
var directLyrics []paxLyrics
if err := json.Unmarshal([]byte(rawJSON), &directLyrics); err == nil && len(directLyrics) > 0 {
return formatPaxContent("Syllable", directLyrics, multiPersonWordByWord), nil
diff --git a/go_backend/lyrics_musixmatch.go b/go_backend/lyrics_musixmatch.go
index 71d4544e..f962a110 100644
--- a/go_backend/lyrics_musixmatch.go
+++ b/go_backend/lyrics_musixmatch.go
@@ -16,7 +16,6 @@ type MusixmatchClient struct {
baseURL string
}
-// Musixmatch proxy response models
type musixmatchSearchResponse struct {
ID int64 `json:"id"`
SongName string `json:"songName"`
@@ -116,7 +115,6 @@ func (c *MusixmatchClient) FetchLyricsInLanguage(songID int64, language string)
return nil, fmt.Errorf("failed to decode musixmatch language response: %w", err)
}
- // Prefer synced lyrics for selected language
if result.SyncedLyrics != nil && strings.TrimSpace(result.SyncedLyrics.Lyrics) != "" {
lines := parseSyncedLyrics(result.SyncedLyrics.Lyrics)
if len(lines) > 0 {
@@ -129,7 +127,6 @@ func (c *MusixmatchClient) FetchLyricsInLanguage(songID int64, language string)
}
}
- // Fall back to unsynced lyrics for selected language
if result.UnsyncedLyrics != nil && strings.TrimSpace(result.UnsyncedLyrics.Lyrics) != "" {
lines := plainTextLyricsLines(result.UnsyncedLyrics.Lyrics)
@@ -162,7 +159,6 @@ func (c *MusixmatchClient) FetchLyrics(trackName, artistName string, durationSec
GoLog("[Musixmatch] Language override '%s' failed: %v\n", preferred, localizedErr)
}
- // Prefer synced lyrics
if result.SyncedLyrics != nil && strings.TrimSpace(result.SyncedLyrics.Lyrics) != "" {
lines := parseSyncedLyrics(result.SyncedLyrics.Lyrics)
if len(lines) > 0 {
@@ -175,7 +171,6 @@ func (c *MusixmatchClient) FetchLyrics(trackName, artistName string, durationSec
}
}
- // Fall back to unsynced lyrics
if result.UnsyncedLyrics != nil && strings.TrimSpace(result.UnsyncedLyrics.Lyrics) != "" {
lines := plainTextLyricsLines(result.UnsyncedLyrics.Lyrics)
diff --git a/go_backend/lyrics_netease.go b/go_backend/lyrics_netease.go
index e9fbf1e6..f6ce6b6c 100644
--- a/go_backend/lyrics_netease.go
+++ b/go_backend/lyrics_netease.go
@@ -15,7 +15,6 @@ type NeteaseClient struct {
httpClient *http.Client
}
-// Netease API response models
type neteaseSearchResponse struct {
Result struct {
Songs []struct {
@@ -172,7 +171,6 @@ func (c *NeteaseClient) FetchLyrics(
return nil, err
}
- // Parse the LRC text into LyricsResponse
lines := parseSyncedLyrics(lrcText)
if len(lines) == 0 {
// May be plain text lyrics without timestamps
diff --git a/go_backend/lyrics_qqmusic.go b/go_backend/lyrics_qqmusic.go
index 0b76a49f..dde3631d 100644
--- a/go_backend/lyrics_qqmusic.go
+++ b/go_backend/lyrics_qqmusic.go
@@ -17,7 +17,6 @@ type QQMusicClient struct {
httpClient *http.Client
}
-// QQ Music search response models
type qqMusicSearchResponse struct {
Data struct {
Song struct {
@@ -184,7 +183,6 @@ func (c *QQMusicClient) FetchLyrics(
}, nil
}
- // Fall back to plain text
resultLines := plainTextLyricsLines(lrcText)
if len(resultLines) > 0 {
diff --git a/go_backend/mobile_deps.go b/go_backend/mobile_deps.go
index bbdb1890..57aaaeec 100644
--- a/go_backend/mobile_deps.go
+++ b/go_backend/mobile_deps.go
@@ -1,10 +1,8 @@
-// mobile_deps.go
// This file ensures gomobile dependencies are not removed by go mod tidy.
// These packages are required by gomobile bind but not directly imported in code.
package gobackend
import (
- // Required for gomobile bind to work
_ "golang.org/x/mobile/bind"
)
diff --git a/go_backend/parallel.go b/go_backend/parallel.go
index b275ade9..2526e139 100644
--- a/go_backend/parallel.go
+++ b/go_backend/parallel.go
@@ -10,7 +10,6 @@ import (
type TrackIDCacheEntry struct {
TidalTrackID int64
QobuzTrackID int64
- AmazonURL string
ExpiresAt time.Time
}
@@ -107,25 +106,6 @@ func (c *TrackIDCache) SetQobuz(isrc string, trackID int64) {
}
}
-func (c *TrackIDCache) SetAmazonURL(isrc string, amazonURL string) {
- c.mu.Lock()
- defer c.mu.Unlock()
-
- entry, exists := c.cache[isrc]
- if !exists {
- entry = &TrackIDCacheEntry{}
- c.cache[isrc] = entry
- }
- entry.AmazonURL = amazonURL
- now := time.Now()
- entry.ExpiresAt = now.Add(c.ttl)
-
- if c.cleanupInterval > 0 && (c.lastCleanup.IsZero() || now.Sub(c.lastCleanup) >= c.cleanupInterval) {
- c.pruneExpiredLocked(now)
- c.lastCleanup = now
- }
-}
-
func (c *TrackIDCache) Clear() {
c.mu.Lock()
defer c.mu.Unlock()
@@ -235,8 +215,6 @@ func PreWarmTrackCache(requests []PreWarmCacheRequest) {
preWarmTidalCache(r.ISRC, r.TrackName, r.ArtistName)
case "qobuz":
preWarmQobuzCache(r.ISRC, r.SpotifyID)
- case "amazon":
- preWarmAmazonCache(r.ISRC, r.SpotifyID)
}
}(req)
}
@@ -256,12 +234,10 @@ func preWarmTidalCache(isrc, _, _ string) {
// 1. From SongLink (fast, no Qobuz API call needed)
// 2. Direct ISRC search on Qobuz API (slower, may fail if ISRC not in Qobuz database)
func preWarmQobuzCache(isrc, spotifyID string) {
- // First, try to get QobuzID from SongLink - this is faster and more reliable
if spotifyID != "" {
client := NewSongLinkClient()
availability, err := client.CheckTrackAvailability(spotifyID, isrc)
if err == nil && availability != nil && availability.QobuzID != "" {
- // Parse QobuzID to int64
var trackID int64
if _, parseErr := fmt.Sscanf(availability.QobuzID, "%d", &trackID); parseErr == nil && trackID > 0 {
GoLog("[Qobuz] Pre-warm cache: Got Qobuz ID %d from SongLink for ISRC %s\n", trackID, isrc)
@@ -271,7 +247,6 @@ func preWarmQobuzCache(isrc, spotifyID string) {
}
}
- // Fallback: Direct ISRC search on Qobuz API
downloader := NewQobuzDownloader()
track, err := downloader.SearchTrackByISRC(isrc)
if err == nil && track != nil {
@@ -280,14 +255,6 @@ func preWarmQobuzCache(isrc, spotifyID string) {
}
}
-func preWarmAmazonCache(isrc, spotifyID string) {
- client := NewSongLinkClient()
- availability, err := client.CheckTrackAvailability(spotifyID, isrc)
- if err == nil && availability != nil && availability.AmazonURL != "" {
- GetTrackIDCache().SetAmazonURL(isrc, availability.AmazonURL)
- }
-}
-
func PreWarmCache(tracksJSON string) error {
var tracks []struct {
ISRC string `json:"isrc"`
diff --git a/go_backend/qobuz.go b/go_backend/qobuz.go
index 015dca8d..092c0e96 100644
--- a/go_backend/qobuz.go
+++ b/go_backend/qobuz.go
@@ -923,18 +923,14 @@ type qobuzAPIResult struct {
duration time.Duration
}
-// Qobuz API timeout configuration
// Mobile networks are more unstable, so we use longer timeouts
const (
qobuzAPITimeoutMobile = 25 * time.Second
- qobuzMaxRetries = 2 // Number of retries per API
+ qobuzMaxRetries = 2
qobuzRetryDelay = 500 * time.Millisecond
)
-// getQobuzAPITimeout returns appropriate timeout based on platform
-// For mobile (gomobile builds), we use longer timeouts
func getQobuzAPITimeout() time.Duration {
- // Since this runs in gomobile context, we always use mobile timeout
// The Go backend is only used on mobile (Android/iOS)
return qobuzAPITimeoutMobile
}
@@ -944,7 +940,6 @@ func fetchQobuzURLWithRetry(provider qobuzAPIProvider, trackID int64, quality st
return fetchQobuzURLSingleAttempt(provider, trackID, quality, timeout, "")
}
-// fetchQobuzURLSingleAttempt fetches download URL with retry logic for a single API+country combination
func fetchQobuzURLSingleAttempt(provider qobuzAPIProvider, trackID int64, quality string, timeout time.Duration, country string) (qobuzDownloadInfo, error) {
var lastErr error
retryDelay := qobuzRetryDelay
@@ -967,7 +962,7 @@ func fetchQobuzURLSingleAttempt(provider qobuzAPIProvider, trackID int64, qualit
if attempt > 0 {
GoLog("[Qobuz] Retry %d/%d for %s after %v\n", attempt, qobuzMaxRetries, provider.Name, retryDelay)
time.Sleep(retryDelay)
- retryDelay *= 2 // Exponential backoff
+ retryDelay *= 2
}
client := NewHTTPClientWithTimeout(timeout)
@@ -1014,11 +1009,10 @@ func fetchQobuzURLSingleAttempt(provider qobuzAPIProvider, trackID int64, qualit
strings.Contains(errStr, "reset") ||
strings.Contains(errStr, "connection refused") ||
strings.Contains(errStr, "eof") {
- continue // Retry
+ continue
}
- break // Non-retryable error
+ break
}
- // Server errors are retryable
if resp.StatusCode >= 500 {
io.Copy(io.Discard, resp.Body)
resp.Body.Close()
@@ -1031,7 +1025,7 @@ func fetchQobuzURLSingleAttempt(provider qobuzAPIProvider, trackID int64, qualit
io.Copy(io.Discard, resp.Body)
resp.Body.Close()
lastErr = fmt.Errorf("rate limited")
- retryDelay = 2 * time.Second // Wait longer for rate limit
+ retryDelay = 2 * time.Second
continue
}
@@ -1308,7 +1302,6 @@ func resolveQobuzTrackForRequest(req DownloadRequest, downloader *QobuzDownloade
track = nil
} else if track != nil {
GoLog("[%s] Successfully found track via SongLink ID: '%s' by '%s'\n", logPrefix, track.Title, track.Performer.Name)
- // Cache for future use
if req.ISRC != "" {
GetTrackIDCache().SetQobuz(req.ISRC, track.ID)
}
diff --git a/go_backend/ratelimit.go b/go_backend/ratelimit.go
index 1f2ac1f6..662d86e4 100644
--- a/go_backend/ratelimit.go
+++ b/go_backend/ratelimit.go
@@ -48,7 +48,6 @@ func (r *RateLimiter) WaitForSlot() {
r.timestamps = append(r.timestamps, time.Now())
}
-// cleanOldTimestamps removes timestamps that are outside the current window
func (r *RateLimiter) cleanOldTimestamps(now time.Time) {
cutoff := now.Add(-r.window)
validStart := 0
diff --git a/go_backend/romaji.go b/go_backend/romaji.go
index d5a73963..3c45d2d9 100644
--- a/go_backend/romaji.go
+++ b/go_backend/romaji.go
@@ -170,11 +170,9 @@ func JapaneseToRomaji(text string) string {
}
func BuildSearchQuery(trackName, artistName string) string {
- // Convert Japanese to romaji
trackRomaji := JapaneseToRomaji(trackName)
artistRomaji := JapaneseToRomaji(artistName)
- // Clean up the query - remove special characters that might interfere with search
trackClean := cleanSearchQuery(trackRomaji)
artistClean := cleanSearchQuery(artistRomaji)
@@ -196,16 +194,13 @@ func cleanSearchQuery(s string) string {
func CleanToASCII(s string) string {
var result strings.Builder
for _, r := range s {
- // Keep only ASCII letters, numbers, spaces, and basic punctuation
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') ||
(r >= '0' && r <= '9') || r == ' ' || r == '-' || r == '\'' {
result.WriteRune(r)
} else if r == ',' || r == '.' {
- // Convert punctuation to space
result.WriteRune(' ')
}
}
- // Clean up multiple spaces
cleaned := strings.Join(strings.Fields(result.String()), " ")
return strings.TrimSpace(cleaned)
}
diff --git a/go_backend/songlink.go b/go_backend/songlink.go
index b5dfd58d..f38a8edb 100644
--- a/go_backend/songlink.go
+++ b/go_backend/songlink.go
@@ -291,7 +291,7 @@ func extractDeezerIDFromURL(deezerURL string) string {
return ""
}
-// extractQobuzIDFromURL extracts Qobuz track ID from URL
+// extractQobuzIDFromURL extracts Qobuz track ID from URL.
// URL formats:
// - https://www.qobuz.com/us-en/album/.../12345678 (album page with track highlight)
// - https://open.qobuz.com/track/12345678
@@ -302,29 +302,24 @@ func extractQobuzIDFromURL(qobuzURL string) string {
return ""
}
- // Try to find /track/ID pattern first
if strings.Contains(qobuzURL, "/track/") {
parts := strings.Split(qobuzURL, "/track/")
if len(parts) > 1 {
idPart := parts[1]
- // Remove query parameters
if idx := strings.Index(idPart, "?"); idx > 0 {
idPart = idPart[:idx]
}
- // Remove trailing slash or path
if idx := strings.Index(idPart, "/"); idx > 0 {
idPart = idPart[:idx]
}
idPart = strings.TrimSpace(idPart)
- // Validate it's a number
if idPart != "" && isNumeric(idPart) {
return idPart
}
}
}
- // Try to extract from album URL with track highlight
- // Format: /album/albumname/trackid or ?trackId=12345678
+ // Try to extract from album URL with track highlight (e.g. ?trackId=12345678)
if strings.Contains(qobuzURL, "trackId=") {
parts := strings.Split(qobuzURL, "trackId=")
if len(parts) > 1 {
@@ -343,7 +338,6 @@ func extractQobuzIDFromURL(qobuzURL string) string {
parts := strings.Split(qobuzURL, "/")
for i := len(parts) - 1; i >= 0; i-- {
part := parts[i]
- // Remove query parameters
if idx := strings.Index(part, "?"); idx > 0 {
part = part[:idx]
}
@@ -386,7 +380,6 @@ func extractYouTubeIDFromURL(youtubeURL string) string {
return ""
}
- // Handle youtu.be short URLs
if strings.Contains(youtubeURL, "youtu.be/") {
parts := strings.Split(youtubeURL, "youtu.be/")
if len(parts) >= 2 {
@@ -401,7 +394,6 @@ func extractYouTubeIDFromURL(youtubeURL string) string {
}
}
- // Handle youtube.com URLs with ?v= parameter
parsed, err := url.Parse(youtubeURL)
if err != nil {
return ""
@@ -411,7 +403,6 @@ func extractYouTubeIDFromURL(youtubeURL string) string {
return v
}
- // Handle /embed/ format
if strings.Contains(parsed.Path, "/embed/") {
parts := strings.Split(parsed.Path, "/embed/")
if len(parts) >= 2 {
@@ -540,7 +531,6 @@ func (s *SongLinkClient) CheckAvailabilityFromDeezer(deezerTrackID string) (*Tra
return availability, nil
}
-// checkAvailabilityFromDeezerSongLink is the original SongLink implementation for Deezer
func (s *SongLinkClient) checkAvailabilityFromDeezerSongLink(deezerTrackID string) (*TrackAvailability, error) {
songLinkRateLimiter.WaitForSlot()
diff --git a/go_backend/spotify.go b/go_backend/spotify.go
index 9728a72f..570c4e24 100644
--- a/go_backend/spotify.go
+++ b/go_backend/spotify.go
@@ -9,7 +9,6 @@ import (
"math/rand"
"net/http"
"net/url"
- "os"
"strings"
"sync"
"time"
@@ -64,45 +63,20 @@ var (
credentialsMu sync.RWMutex
)
-var ErrNoSpotifyCredentials = errors.New("Spotify credentials not configured. Please set your own Client ID and Secret in Settings, or use Deezer as metadata source (free, no credentials required)")
+var ErrNoSpotifyCredentials = errors.New("built-in Spotify API metadata provider has been removed; use Deezer or the spotify-web extension instead")
func SetSpotifyCredentials(clientID, clientSecret string) {
credentialsMu.Lock()
defer credentialsMu.Unlock()
- customClientID = clientID
- customClientSecret = clientSecret
+ customClientID = ""
+ customClientSecret = ""
}
func HasSpotifyCredentials() bool {
- credentialsMu.RLock()
- defer credentialsMu.RUnlock()
-
- if customClientID != "" && customClientSecret != "" {
- return true
- }
-
- if os.Getenv("SPOTIFY_CLIENT_ID") != "" && os.Getenv("SPOTIFY_CLIENT_SECRET") != "" {
- return true
- }
-
return false
}
func getCredentials() (string, string, error) {
- credentialsMu.RLock()
- defer credentialsMu.RUnlock()
-
- if customClientID != "" && customClientSecret != "" {
- return customClientID, customClientSecret, nil
- }
-
- clientID := os.Getenv("SPOTIFY_CLIENT_ID")
- clientSecret := os.Getenv("SPOTIFY_CLIENT_SECRET")
-
- if clientID != "" && clientSecret != "" {
- return clientID, clientSecret, nil
- }
-
return "", "", ErrNoSpotifyCredentials
}
diff --git a/go_backend/tidal.go b/go_backend/tidal.go
index 22fd2377..ea9a8886 100644
--- a/go_backend/tidal.go
+++ b/go_backend/tidal.go
@@ -103,7 +103,7 @@ type MPD struct {
func NewTidalDownloader() *TidalDownloader {
tidalDownloaderOnce.Do(func() {
globalTidalDownloader = &TidalDownloader{
- client: NewHTTPClientWithTimeout(DefaultTimeout), // 60s timeout
+ client: NewHTTPClientWithTimeout(DefaultTimeout),
}
apis := globalTidalDownloader.GetAvailableAPIs()
@@ -116,7 +116,7 @@ func NewTidalDownloader() *TidalDownloader {
func (t *TidalDownloader) GetAvailableAPIs() []string {
return []string{
- "https://tidal-api.binimum.org", // priority
+ "https://tidal-api.binimum.org",
"https://tidal.kinoplus.online",
"https://triton.squid.wtf",
"https://vogel.qqdl.site",
@@ -195,7 +195,6 @@ func (t *TidalDownloader) SearchTrackByISRC(isrc string) (*TidalTrack, error) {
return nil, fmt.Errorf("tidal ISRC search API disabled: no client credentials mode")
}
-// Now includes romaji conversion for Japanese text (4 search strategies like PC)
func (t *TidalDownloader) SearchTrackByMetadataWithISRC(trackName, artistName, albumName, spotifyISRC string, expectedDuration int) (*TidalTrack, error) {
return nil, fmt.Errorf("tidal metadata search API disabled: no client credentials mode")
}
@@ -204,7 +203,6 @@ func (t *TidalDownloader) SearchTrackByMetadata(trackName, artistName string) (*
return nil, fmt.Errorf("tidal metadata search API disabled: no client credentials mode")
}
-// TidalDownloadInfo contains download URL and quality info
type TidalDownloadInfo struct {
URL string
BitDepth int
@@ -218,15 +216,13 @@ type tidalAPIResult struct {
duration time.Duration
}
-// Tidal API timeout configuration
// Mobile networks are more unstable, so we use longer timeouts
const (
tidalAPITimeoutMobile = 25 * time.Second
- tidalMaxRetries = 2 // Number of retries per API
+ tidalMaxRetries = 2
tidalRetryDelay = 500 * time.Millisecond
)
-// fetchTidalURLWithRetry fetches download URL from a single Tidal API with retry logic
func fetchTidalURLWithRetry(api string, trackID int64, quality string, timeout time.Duration) (TidalDownloadInfo, error) {
var lastErr error
retryDelay := tidalRetryDelay
@@ -235,7 +231,7 @@ func fetchTidalURLWithRetry(api string, trackID int64, quality string, timeout t
if attempt > 0 {
GoLog("[Tidal] Retry %d/%d for %s after %v\n", attempt, tidalMaxRetries, api, retryDelay)
time.Sleep(retryDelay)
- retryDelay *= 2 // Exponential backoff
+ retryDelay *= 2
}
client := NewHTTPClientWithTimeout(timeout)
@@ -250,17 +246,15 @@ func fetchTidalURLWithRetry(api string, trackID int64, quality string, timeout t
resp, err := client.Do(req)
if err != nil {
lastErr = err
- // Check for retryable errors (timeout, connection reset)
errStr := strings.ToLower(err.Error())
if strings.Contains(errStr, "timeout") ||
strings.Contains(errStr, "reset") ||
strings.Contains(errStr, "connection refused") ||
strings.Contains(errStr, "eof") {
- continue // Retry
+ continue
}
- break // Non-retryable error
+ break
}
- // Server errors are retryable
if resp.StatusCode >= 500 {
io.Copy(io.Discard, resp.Body)
resp.Body.Close()
@@ -273,7 +267,7 @@ func fetchTidalURLWithRetry(api string, trackID int64, quality string, timeout t
io.Copy(io.Discard, resp.Body)
resp.Body.Close()
lastErr = fmt.Errorf("rate limited")
- retryDelay = 2 * time.Second // Wait longer for rate limit
+ retryDelay = 2 * time.Second
continue
}
diff --git a/go_backend/youtube.go b/go_backend/youtube.go
index bfbedcbb..e43d0e39 100644
--- a/go_backend/youtube.go
+++ b/go_backend/youtube.go
@@ -1,4 +1,3 @@
-// Package gobackend - YouTube download via Cobalt API (lossy-only provider)
package gobackend
import (
@@ -161,7 +160,6 @@ func parseYouTubeQualityInput(raw string) (format string, bitrate int, normalize
}
}
-// SearchYouTube returns a YouTube Music search URL for the given track
func (y *YouTubeDownloader) SearchYouTube(trackName, artistName string) (string, error) {
query := fmt.Sprintf("%s %s", artistName, trackName)
searchQuery := url.QueryEscape(query)
@@ -213,7 +211,6 @@ func (y *YouTubeDownloader) GetDownloadURL(youtubeURL string, quality YouTubeQua
return resp, nil
}
-// requestCobaltDirect sends a download request to the primary Cobalt API.
func (y *YouTubeDownloader) requestCobaltDirect(videoURL, audioFormat, audioBitrate string) (*CobaltResponse, error) {
reqBody := CobaltRequest{
URL: videoURL,
@@ -470,7 +467,6 @@ func BuildYouTubeWatchURL(videoID string) string {
return fmt.Sprintf("https://music.youtube.com/watch?v=%s", videoID)
}
-// isYouTubeVideoID checks if s is an 11-char YouTube video ID
func isYouTubeVideoID(s string) bool {
if len(s) != 11 {
return false
@@ -707,7 +703,6 @@ func downloadFromYouTube(req DownloadRequest) (YouTubeDownloadResult, error) {
GoLog("[YouTube] Downloading to: %s\n", outputPath)
- // Parallel fetch cover art + lyrics
var parallelResult *ParallelDownloadResult
if req.EmbedLyrics || req.CoverURL != "" {
GoLog("[YouTube] Starting parallel fetch for cover and lyrics...\n")
diff --git a/image.png b/image.png
deleted file mode 100644
index 565fab55..00000000
Binary files a/image.png and /dev/null differ
diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift
index 8dfaa378..b85020da 100644
--- a/ios/Runner/AppDelegate.swift
+++ b/ios/Runner/AppDelegate.swift
@@ -15,6 +15,9 @@ import Gobackend // Import Go framework
private var libraryScanProgressEventSink: FlutterEventSink?
private var lastLibraryScanProgressPayload: String?
+ /// Currently accessed security-scoped URL for library folder
+ private var activeSecurityScopedURL: URL?
+
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
@@ -492,13 +495,6 @@ import Gobackend // Import Go framework
if let error = error { throw error }
return response
- case "getAmazonURLFromDeezerTrack":
- let args = call.arguments as! [String: Any]
- let deezerTrackId = args["deezer_track_id"] as! String
- let response = GobackendGetAmazonURLFromDeezerTrack(deezerTrackId, &error)
- if let error = error { throw error }
- return response
-
case "preWarmTrackCache":
let args = call.arguments as! [String: Any]
let tracksJson = args["tracks"] as! String
@@ -922,6 +918,26 @@ import Gobackend // Import Go framework
let response = GobackendReadAudioMetadataJSON(filePath, &error)
if let error = error { throw error }
return response
+
+ // iOS Security-Scoped Bookmark for Local Library
+ case "resolveIosBookmark":
+ let args = call.arguments as! [String: Any]
+ let bookmarkBase64 = args["bookmark"] as! String
+ return try resolveIosBookmark(bookmarkBase64)
+
+ case "startAccessingIosBookmark":
+ let args = call.arguments as! [String: Any]
+ let bookmarkBase64 = args["bookmark"] as! String
+ return try startAccessingIosBookmark(bookmarkBase64)
+
+ case "stopAccessingIosBookmark":
+ stopAccessingIosBookmark()
+ return nil
+
+ case "createIosBookmarkFromPath":
+ let args = call.arguments as! [String: Any]
+ let path = args["path"] as! String
+ return try createIosBookmarkFromPath(path)
// Lyrics Provider Settings
case "setLyricsProviders":
@@ -953,6 +969,15 @@ import Gobackend // Import Go framework
if let error = error { throw error }
return response
+ // CUE Sheet Parsing
+ case "parseCueSheet":
+ let args = call.arguments as! [String: Any]
+ let cuePath = args["cue_path"] as! String
+ let audioDir = args["audio_dir"] as? String ?? ""
+ let response = GobackendParseCueSheet(cuePath, audioDir, &error)
+ if let error = error { throw error }
+ return response
+
default:
throw NSError(
domain: "SpotiFLAC",
@@ -961,6 +986,112 @@ import Gobackend // Import Go framework
)
}
}
+
+ // MARK: - iOS Security-Scoped Bookmark Helpers
+
+ /// Create a security-scoped bookmark from a filesystem path (e.g. from FilePicker).
+ /// The path must currently be accessible (within the same picker session).
+ /// Returns base64-encoded bookmark data.
+ private func createIosBookmarkFromPath(_ path: String) throws -> String {
+ let url = URL(fileURLWithPath: path)
+ do {
+ let bookmarkData = try url.bookmarkData(
+ options: .minimalBookmark,
+ includingResourceValuesForKeys: nil,
+ relativeTo: nil
+ )
+ return bookmarkData.base64EncodedString()
+ } catch {
+ throw NSError(
+ domain: "SpotiFLAC",
+ code: -1,
+ userInfo: [NSLocalizedDescriptionKey: "Failed to create bookmark for path \(path): \(error.localizedDescription)"]
+ )
+ }
+ }
+
+ /// Resolve a base64-encoded security-scoped bookmark and return the resolved path.
+ /// Does NOT start accessing the resource.
+ private func resolveIosBookmark(_ bookmarkBase64: String) throws -> String {
+ guard let bookmarkData = Data(base64Encoded: bookmarkBase64) else {
+ throw NSError(
+ domain: "SpotiFLAC",
+ code: -1,
+ userInfo: [NSLocalizedDescriptionKey: "Invalid base64 bookmark data"]
+ )
+ }
+
+ var isStale = false
+ let url: URL
+ do {
+ url = try URL(
+ resolvingBookmarkData: bookmarkData,
+ options: [],
+ relativeTo: nil,
+ bookmarkDataIsStale: &isStale
+ )
+ } catch {
+ throw NSError(
+ domain: "SpotiFLAC",
+ code: -1,
+ userInfo: [NSLocalizedDescriptionKey: "Failed to resolve bookmark: \(error.localizedDescription)"]
+ )
+ }
+
+ return url.path
+ }
+
+ /// Resolve a base64-encoded bookmark, start accessing the security-scoped resource,
+ /// and return the resolved filesystem path. The resource stays accessed until
+ /// `stopAccessingIosBookmark()` is called.
+ private func startAccessingIosBookmark(_ bookmarkBase64: String) throws -> String {
+ // Stop any previously accessed resource first
+ stopAccessingIosBookmark()
+
+ guard let bookmarkData = Data(base64Encoded: bookmarkBase64) else {
+ throw NSError(
+ domain: "SpotiFLAC",
+ code: -1,
+ userInfo: [NSLocalizedDescriptionKey: "Invalid base64 bookmark data"]
+ )
+ }
+
+ var isStale = false
+ let url: URL
+ do {
+ url = try URL(
+ resolvingBookmarkData: bookmarkData,
+ options: [],
+ relativeTo: nil,
+ bookmarkDataIsStale: &isStale
+ )
+ } catch {
+ throw NSError(
+ domain: "SpotiFLAC",
+ code: -1,
+ userInfo: [NSLocalizedDescriptionKey: "Failed to resolve bookmark: \(error.localizedDescription)"]
+ )
+ }
+
+ guard url.startAccessingSecurityScopedResource() else {
+ throw NSError(
+ domain: "SpotiFLAC",
+ code: -1,
+ userInfo: [NSLocalizedDescriptionKey: "Failed to start accessing security-scoped resource at \(url.path)"]
+ )
+ }
+
+ activeSecurityScopedURL = url
+ return url.path
+ }
+
+ /// Stop accessing the currently active security-scoped resource, if any.
+ private func stopAccessingIosBookmark() {
+ if let url = activeSecurityScopedURL {
+ url.stopAccessingSecurityScopedResource()
+ activeSecurityScopedURL = nil
+ }
+ }
}
private final class ClosureStreamHandler: NSObject, FlutterStreamHandler {
diff --git a/lib/app.dart b/lib/app.dart
index 981c3bae..cf0ec25b 100644
--- a/lib/app.dart
+++ b/lib/app.dart
@@ -17,7 +17,6 @@ final _routerProvider = Provider((ref) {
settingsProvider.select((s) => s.hasCompletedTutorial),
);
- // Determine initial location based on app state
String initialLocation;
if (isFirstLaunch) {
initialLocation = '/setup';
diff --git a/lib/constants/app_info.dart b/lib/constants/app_info.dart
index 48a1c1e2..d423fac6 100644
--- a/lib/constants/app_info.dart
+++ b/lib/constants/app_info.dart
@@ -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.7.1';
- static const String buildNumber = '104';
+ static const String version = '3.7.2';
+ static const String buildNumber = '105';
static const String fullVersion = '$version+$buildNumber';
diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart
index b321656f..488563dc 100644
--- a/lib/l10n/app_localizations.dart
+++ b/lib/l10n/app_localizations.dart
@@ -763,7 +763,7 @@ abstract class AppLocalizations {
/// App description in header card
///
/// In en, this message translates to:
- /// **'Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.'**
+ /// **'Download Spotify tracks in lossless quality from Tidal and Qobuz.'**
String get aboutAppDescription;
/// Section header for artist albums
@@ -1306,6 +1306,24 @@ abstract class AppLocalizations {
/// **'No tracks found'**
String get errorNoTracksFound;
+ /// Error title - URL not handled by any extension or service
+ ///
+ /// In en, this message translates to:
+ /// **'Link not recognized'**
+ String get errorUrlNotRecognized;
+
+ /// Error message - URL not recognized explanation
+ ///
+ /// In en, this message translates to:
+ /// **'This link is not supported. Make sure the URL is correct and a compatible extension is installed.'**
+ String get errorUrlNotRecognizedMessage;
+
+ /// Error message - generic URL fetch failure
+ ///
+ /// In en, this message translates to:
+ /// **'Failed to load content from this link. Please try again.'**
+ String get errorUrlFetchFailed;
+
/// Error - extension source not available
///
/// In en, this message translates to:
@@ -1438,6 +1456,18 @@ abstract class AppLocalizations {
/// **'No organization'**
String get folderOrganizationNone;
+ /// Folder option - playlist folders
+ ///
+ /// In en, this message translates to:
+ /// **'By Playlist'**
+ String get folderOrganizationByPlaylist;
+
+ /// Subtitle for playlist folder option
+ ///
+ /// In en, this message translates to:
+ /// **'Separate folder for each playlist'**
+ String get folderOrganizationByPlaylistSubtitle;
+
/// Folder option - artist folders
///
/// In en, this message translates to:
@@ -1576,7 +1606,7 @@ abstract class AppLocalizations {
/// **'If a track is not available on the first provider, the app will automatically try the next one.'**
String get providerPriorityInfo;
- /// Label for built-in providers (Tidal/Qobuz/Amazon)
+ /// Label for built-in providers (Tidal/Qobuz)
///
/// In en, this message translates to:
/// **'Built-in'**
@@ -3271,7 +3301,7 @@ abstract class AppLocalizations {
/// Tutorial welcome tip 2
///
/// In en, this message translates to:
- /// **'Get FLAC quality audio from Tidal, Qobuz, or Amazon Music'**
+ /// **'Get FLAC quality audio from Tidal, Qobuz, or Deezer'**
String get tutorialWelcomeTip2;
/// Tutorial welcome tip 3
@@ -3794,6 +3824,78 @@ abstract class AppLocalizations {
/// **'Conversion failed'**
String get trackConvertFailed;
+ /// Title for CUE split bottom sheet
+ ///
+ /// In en, this message translates to:
+ /// **'Split CUE Sheet'**
+ String get cueSplitTitle;
+
+ /// Subtitle for CUE split menu item
+ ///
+ /// In en, this message translates to:
+ /// **'Split CUE+FLAC into individual tracks'**
+ String get cueSplitSubtitle;
+
+ /// Album name in CUE split sheet
+ ///
+ /// In en, this message translates to:
+ /// **'Album: {album}'**
+ String cueSplitAlbum(String album);
+
+ /// Artist name in CUE split sheet
+ ///
+ /// In en, this message translates to:
+ /// **'Artist: {artist}'**
+ String cueSplitArtist(String artist);
+
+ /// Number of tracks in CUE sheet
+ ///
+ /// In en, this message translates to:
+ /// **'{count} tracks'**
+ String cueSplitTrackCount(int count);
+
+ /// CUE split confirmation dialog title
+ ///
+ /// In en, this message translates to:
+ /// **'Split CUE Album'**
+ String get cueSplitConfirmTitle;
+
+ /// CUE split confirmation dialog message
+ ///
+ /// In en, this message translates to:
+ /// **'Split \"{album}\" into {count} individual FLAC files?\n\nFiles will be saved to the same directory.'**
+ String cueSplitConfirmMessage(String album, int count);
+
+ /// Snackbar while splitting CUE
+ ///
+ /// In en, this message translates to:
+ /// **'Splitting CUE sheet... ({current}/{total})'**
+ String cueSplitSplitting(int current, int total);
+
+ /// Snackbar after successful CUE split
+ ///
+ /// In en, this message translates to:
+ /// **'Split into {count} tracks successfully'**
+ String cueSplitSuccess(int count);
+
+ /// Snackbar when CUE split fails
+ ///
+ /// In en, this message translates to:
+ /// **'CUE split failed'**
+ String get cueSplitFailed;
+
+ /// Error when CUE audio file is missing
+ ///
+ /// In en, this message translates to:
+ /// **'Audio file not found for this CUE sheet'**
+ String get cueSplitNoAudioFile;
+
+ /// Button text to start CUE splitting
+ ///
+ /// In en, this message translates to:
+ /// **'Split into Tracks'**
+ String get cueSplitButton;
+
/// Generic action button - create
///
/// In en, this message translates to:
diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart
index 2b846be4..4cb5b75d 100644
--- a/lib/l10n/app_localizations_de.dart
+++ b/lib/l10n/app_localizations_de.dart
@@ -15,7 +15,7 @@ class AppLocalizationsDe extends AppLocalizations {
String get navHome => 'Startseite';
@override
- String get navLibrary => 'Archiv';
+ String get navLibrary => 'Bibliothek';
@override
String get navSettings => 'Einstellungen';
@@ -500,7 +500,7 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get setupNotificationBackgroundDescription =>
- 'Werde benachrichtigt über Download-Fortschritt und -Fertigstellung. Dies hilft Ihnen, Downloads zu verfolgen, wenn die App im Hintergrund ist.';
+ 'Erhalte Benachrichtigungen über den Fortschritt und die Fertigstellung deiner Downloads, selbst wenn die App im Hintergrund läuft.';
@override
String get setupSkipForNow => 'Vorerst überspringen';
@@ -592,11 +592,11 @@ class AppLocalizationsDe extends AppLocalizations {
}
@override
- String get dialogImportPlaylistTitle => 'Wiedergabeliste importieren';
+ String get dialogImportPlaylistTitle => 'Playlist importieren';
@override
String dialogImportPlaylistMessage(int count) {
- return '$count Titel in CSV gefunden. Zur Warteschlange hinzufügen?';
+ return '$count Titel gefunden hinzufügen?';
}
@override
@@ -606,12 +606,12 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String snackbarAddedToQueue(String trackName) {
- return '\"$trackName\" zur Warteschlange hinzugefügt';
+ return '\"$trackName\" hinzugefügt';
}
@override
String snackbarAddedTracksToQueue(int count) {
- return '$count Titel zur Warteschlange hinzugefügt';
+ return '$count Titel hinzugefügt';
}
@override
@@ -701,6 +701,17 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get errorNoTracksFound => 'Keine Titel gefunden';
+ @override
+ String get errorUrlNotRecognized => 'Link not recognized';
+
+ @override
+ String get errorUrlNotRecognizedMessage =>
+ 'This link is not supported. Make sure the URL is correct and a compatible extension is installed.';
+
+ @override
+ String get errorUrlFetchFailed =>
+ 'Failed to load content from this link. Please try again.';
+
@override
String errorMissingExtensionSource(String item) {
return 'Kann $item nicht lade wegen fehlender Erweiterungsquelle';
@@ -765,15 +776,22 @@ class AppLocalizationsDe extends AppLocalizations {
String get filenameFormat => 'Dateinamenformat';
@override
- String get filenameShowAdvancedTags => 'Show advanced tags';
+ String get filenameShowAdvancedTags => 'Erweiterte Tags anzeigen';
@override
String get filenameShowAdvancedTagsDescription =>
- 'Enable formatted tags for track padding and date patterns';
+ 'Formatierte Tags für Track-Padding und Datumsmuster aktivieren';
@override
String get folderOrganizationNone => 'Keine Organisation';
+ @override
+ String get folderOrganizationByPlaylist => 'By Playlist';
+
+ @override
+ String get folderOrganizationByPlaylistSubtitle =>
+ 'Separate folder for each playlist';
+
@override
String get folderOrganizationByArtist => 'Nach Künstler';
@@ -918,11 +936,11 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String logEntries(int count) {
- return 'Entries ($count)';
+ return '$count Einträge';
}
@override
- String get credentialsTitle => 'Spotify Credentials';
+ String get credentialsTitle => 'Spotify-Anmeldedaten';
@override
String get credentialsDescription =>
@@ -984,7 +1002,7 @@ class AppLocalizationsDe extends AppLocalizations {
'Wähle wie Songtexte mit deinen Downloads gespeichert werden';
@override
- String get lyricsModeEmbed => 'In Datei einbinden';
+ String get lyricsModeEmbed => 'In Datei einbetten';
@override
String get lyricsModeEmbedSubtitle => 'Lyrics in FLAC Metadaten gespeichert';
@@ -1001,7 +1019,7 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get lyricsModeBothSubtitle =>
- 'Lyrics einbinden und als .lrc speichern';
+ 'Lyrics einbetten und als .lrc speichern';
@override
String get sectionColor => 'Farbe';
@@ -1019,29 +1037,30 @@ class AppLocalizationsDe extends AppLocalizations {
String get appearanceLanguage => 'App Sprache';
@override
- String get settingsAppearanceSubtitle => 'Theme, colors, display';
+ String get settingsAppearanceSubtitle => 'Design, Farben, Anzeige';
@override
- String get settingsDownloadSubtitle => 'Service, quality, filename format';
+ String get settingsDownloadSubtitle => 'Dienst, Qualität, Dateinamen-Format';
@override
- String get settingsOptionsSubtitle => 'Fallback, lyrics, cover art, updates';
+ String get settingsOptionsSubtitle => 'Fallback, Lyrics, Covers, Updates';
@override
- String get settingsExtensionsSubtitle => 'Manage download providers';
+ String get settingsExtensionsSubtitle => 'Download-Anbieter verwalten';
@override
- String get settingsLogsSubtitle => 'View app logs for debugging';
+ String get settingsLogsSubtitle => 'App-Logs zum Debuggen anzeigen';
@override
- String get loadingSharedLink => 'Loading shared link...';
+ String get loadingSharedLink => 'Link wird geladen...';
@override
- String get pressBackAgainToExit => 'Press back again to exit';
+ String get pressBackAgainToExit =>
+ 'Drücke wieder \"zurück\" um die App zu beenden';
@override
String downloadAllCount(int count) {
- return 'Download All ($count)';
+ return 'Alle $count Titel herunterladen';
}
@override
@@ -1049,65 +1068,65 @@ class AppLocalizationsDe extends AppLocalizations {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
- other: '$count tracks',
- one: '1 track',
+ other: '$count Titel',
+ one: '1 Titel',
);
return '$_temp0';
}
@override
- String get trackCopyFilePath => 'Copy file path';
+ String get trackCopyFilePath => 'Dateipfad kopieren';
@override
- String get trackRemoveFromDevice => 'Remove from device';
+ String get trackRemoveFromDevice => 'Vom Gerät entfernen';
@override
- String get trackLoadLyrics => 'Load Lyrics';
+ String get trackLoadLyrics => 'Lade Lyrics';
@override
- String get trackMetadata => 'Metadata';
+ String get trackMetadata => 'Metadaten';
@override
- String get trackFileInfo => 'File Info';
+ String get trackFileInfo => 'Datei-Info';
@override
String get trackLyrics => 'Lyrics';
@override
- String get trackFileNotFound => 'File not found';
+ String get trackFileNotFound => 'Datei nicht gefunden';
@override
- String get trackOpenInDeezer => 'Open in Deezer';
+ String get trackOpenInDeezer => 'In Deezer öffnen';
@override
- String get trackOpenInSpotify => 'Open in Spotify';
+ String get trackOpenInSpotify => 'In Spotify öffnen';
@override
- String get trackTrackName => 'Track name';
+ String get trackTrackName => 'Name des Titels';
@override
- String get trackArtist => 'Artist';
+ String get trackArtist => 'Künstler';
@override
- String get trackAlbumArtist => 'Album artist';
+ String get trackAlbumArtist => 'Album Künstler';
@override
String get trackAlbum => 'Album';
@override
- String get trackTrackNumber => 'Track number';
+ String get trackTrackNumber => 'Titelnummer';
@override
- String get trackDiscNumber => 'Disc number';
+ String get trackDiscNumber => 'CD-Nummer';
@override
- String get trackDuration => 'Duration';
+ String get trackDuration => 'Länge';
@override
- String get trackAudioQuality => 'Audio quality';
+ String get trackAudioQuality => 'Audioqualität';
@override
- String get trackReleaseDate => 'Release date';
+ String get trackReleaseDate => 'Erscheinungsdatum';
@override
String get trackGenre => 'Genre';
@@ -1116,71 +1135,73 @@ class AppLocalizationsDe extends AppLocalizations {
String get trackLabel => 'Label';
@override
- String get trackCopyright => 'Copyright';
+ String get trackCopyright => 'Urheberrecht';
@override
- String get trackDownloaded => 'Downloaded';
+ String get trackDownloaded => 'Heruntergeladen';
@override
- String get trackCopyLyrics => 'Copy lyrics';
+ String get trackCopyLyrics => 'Lyrics kopieren';
@override
- String get trackLyricsNotAvailable => 'Lyrics not available for this track';
+ String get trackLyricsNotAvailable =>
+ 'Lyrics sind für diesen Titel nicht verfügbar';
@override
- String get trackLyricsTimeout => 'Request timed out. Try again later.';
+ String get trackLyricsTimeout =>
+ 'Anfrage Timeout. Versuche es später erneut.';
@override
- String get trackLyricsLoadFailed => 'Failed to load lyrics';
+ String get trackLyricsLoadFailed => 'Fehler beim Laden der Lyrics';
@override
- String get trackEmbedLyrics => 'Embed Lyrics';
+ String get trackEmbedLyrics => 'Lyrics einbetten';
@override
- String get trackLyricsEmbedded => 'Lyrics embedded successfully';
+ String get trackLyricsEmbedded => 'Lyrics erfolgreich eingebettet';
@override
- String get trackInstrumental => 'Instrumental track';
+ String get trackInstrumental => 'Instrumentalspur';
@override
- String get trackCopiedToClipboard => 'Copied to clipboard';
+ String get trackCopiedToClipboard => 'In Zwischenablage kopiert';
@override
- String get trackDeleteConfirmTitle => 'Remove from device?';
+ String get trackDeleteConfirmTitle => 'Vom Gerät entfernen?';
@override
String get trackDeleteConfirmMessage =>
- 'This will permanently delete the downloaded file and remove it from your history.';
+ 'Dies wird die heruntergeladene Datei dauerhaft löschen und sie aus deinem Verlauf entfernen.';
@override
- String get dateToday => 'Today';
+ String get dateToday => 'Heute';
@override
- String get dateYesterday => 'Yesterday';
+ String get dateYesterday => 'Gestern';
@override
String dateDaysAgo(int count) {
- return '$count days ago';
+ return 'Vor $count Tagen';
}
@override
String dateWeeksAgo(int count) {
- return '$count weeks ago';
+ return 'Vor $count Wochen';
}
@override
String dateMonthsAgo(int count) {
- return '$count months ago';
+ return 'Vor $count Monaten';
}
@override
- String get storeFilterAll => 'All';
+ String get storeFilterAll => 'Alle';
@override
- String get storeFilterMetadata => 'Metadata';
+ String get storeFilterMetadata => 'Metadaten';
@override
- String get storeFilterDownload => 'Download';
+ String get storeFilterDownload => 'Herunterladen';
@override
String get storeFilterUtility => 'Utility';
@@ -1192,142 +1213,146 @@ class AppLocalizationsDe extends AppLocalizations {
String get storeFilterIntegration => 'Integration';
@override
- String get storeClearFilters => 'Clear filters';
+ String get storeClearFilters => 'Filter entfernen';
@override
- String get extensionDefaultProvider => 'Default (Deezer/Spotify)';
+ String get extensionDefaultProvider => 'Standard (Deezer/Spotify)';
@override
- String get extensionDefaultProviderSubtitle => 'Use built-in search';
+ String get extensionDefaultProviderSubtitle => 'Eingebaute Suche verwenden';
@override
- String get extensionAuthor => 'Author';
+ String get extensionAuthor => 'Entwickler';
@override
String get extensionId => 'ID';
@override
- String get extensionError => 'Error';
+ String get extensionError => 'Fehler';
@override
- String get extensionCapabilities => 'Capabilities';
+ String get extensionCapabilities => 'Eigenschaften';
@override
- String get extensionMetadataProvider => 'Metadata Provider';
+ String get extensionMetadataProvider => 'Metadaten-Anbieter';
@override
- String get extensionDownloadProvider => 'Download Provider';
+ String get extensionDownloadProvider => 'Download-Anbieter';
@override
- String get extensionLyricsProvider => 'Lyrics Provider';
+ String get extensionLyricsProvider => 'Lyrics-Anbieter';
@override
String get extensionUrlHandler => 'URL Handler';
@override
- String get extensionQualityOptions => 'Quality Options';
+ String get extensionQualityOptions => 'Qualitätsoptionen';
@override
String get extensionPostProcessingHooks => 'Post-Processing Hooks';
@override
- String get extensionPermissions => 'Permissions';
+ String get extensionPermissions => 'Berechtigungen';
@override
- String get extensionSettings => 'Settings';
+ String get extensionSettings => 'Einstellungen';
@override
- String get extensionRemoveButton => 'Remove Extension';
+ String get extensionRemoveButton => 'Erweiterung entfernen';
@override
- String get extensionUpdated => 'Updated';
+ String get extensionUpdated => 'Aktualisiert';
@override
- String get extensionMinAppVersion => 'Min App Version';
+ String get extensionMinAppVersion => 'Min App-Version';
@override
- String get extensionCustomTrackMatching => 'Custom Track Matching';
+ String get extensionCustomTrackMatching =>
+ 'Benutzerdefiniertes Track-Matching';
@override
- String get extensionPostProcessing => 'Post-Processing';
+ String get extensionPostProcessing => 'Post-processing';
@override
String extensionHooksAvailable(int count) {
- return '$count hook(s) available';
+ return '$count Hook(s) verfügbar';
}
@override
String extensionPatternsCount(int count) {
- return '$count pattern(s)';
+ return '$count Muster';
}
@override
String extensionStrategy(String strategy) {
- return 'Strategy: $strategy';
+ return 'Strategie: $strategy';
}
@override
- String get extensionsProviderPrioritySection => 'Provider Priority';
+ String get extensionsProviderPrioritySection => 'Provider-Priorität';
@override
- String get extensionsInstalledSection => 'Installed Extensions';
+ String get extensionsInstalledSection => 'Installierte Erweiterungen';
@override
- String get extensionsNoExtensions => 'No extensions installed';
+ String get extensionsNoExtensions => 'Keine Erweiterungen installiert';
@override
String get extensionsNoExtensionsSubtitle =>
- 'Install .spotiflac-ext files to add new providers';
+ 'Installiere .spotiflac-ext Dateien um neue Anbieter hinzuzufügen';
@override
- String get extensionsInstallButton => 'Install Extension';
+ String get extensionsInstallButton => 'Erweiterung installieren';
@override
String get extensionsInfoTip =>
- 'Extensions can add new metadata and download providers. Only install extensions from trusted sources.';
+ 'Erweiterungen können neue Metadaten und Download-Anbieter hinzufügen. Installiere nur Erweiterungen von vertrauenswürdigen Quellen.';
@override
- String get extensionsInstalledSuccess => 'Extension installed successfully';
+ String get extensionsInstalledSuccess =>
+ 'Erweiterung erfolgreich installiert';
@override
- String get extensionsDownloadPriority => 'Download Priority';
+ String get extensionsDownloadPriority => 'Download-Priorität';
@override
- String get extensionsDownloadPrioritySubtitle => 'Set download service order';
+ String get extensionsDownloadPrioritySubtitle =>
+ 'Download-Service-Reihenfolge festlegen';
@override
String get extensionsNoDownloadProvider =>
- 'No extensions with download provider';
+ 'Keine Erweiterungen mit Download-Provider';
@override
- String get extensionsMetadataPriority => 'Metadata Priority';
+ String get extensionsMetadataPriority => 'Metadaten Priorität';
@override
String get extensionsMetadataPrioritySubtitle =>
- 'Set search & metadata source order';
+ 'Reihenfolge der Such- und Metadaten quellen festlegen';
@override
String get extensionsNoMetadataProvider =>
- 'No extensions with metadata provider';
+ 'Keine Erweiterungen mit Metadaten-Anbieter';
@override
- String get extensionsSearchProvider => 'Search Provider';
+ String get extensionsSearchProvider => 'Such-Provider';
@override
- String get extensionsNoCustomSearch => 'No extensions with custom search';
+ String get extensionsNoCustomSearch =>
+ 'Keine Erweiterungen mit benutzerdefinierter Suche';
@override
String get extensionsSearchProviderDescription =>
- 'Choose which service to use for searching tracks';
+ 'Wähle den Dienst für die Suche von Titel';
@override
- String get extensionsCustomSearch => 'Custom search';
+ String get extensionsCustomSearch => 'Benutzerdefinierte Suche';
@override
- String get extensionsErrorLoading => 'Error loading extension';
+ String get extensionsErrorLoading => 'Fehler beim Laden der Erweiterung';
@override
- String get qualityFlacLossless => 'FLAC Lossless';
+ String get qualityFlacLossless => 'FLAC Verlustfrei';
@override
String get qualityFlacLosslessSubtitle => '16-bit / 44.1kHz';
@@ -1336,21 +1361,21 @@ class AppLocalizationsDe extends AppLocalizations {
String get qualityHiResFlac => 'Hi-Res FLAC';
@override
- String get qualityHiResFlacSubtitle => '24-bit / up to 96kHz';
+ String get qualityHiResFlacSubtitle => '24-Bit / bis 96kHz';
@override
String get qualityHiResFlacMax => 'Hi-Res FLAC Max';
@override
- String get qualityHiResFlacMaxSubtitle => '24-bit / up to 192kHz';
+ String get qualityHiResFlacMaxSubtitle => '24-Bit / bis 192kHz';
@override
String get qualityNote =>
- 'Actual quality depends on track availability from the service';
+ 'Die eigentliche Qualität hängt von der Verfügbarkeit des Dienstes ab';
@override
String get youtubeQualityNote =>
- 'YouTube provides lossy audio only. Not part of lossless fallback.';
+ 'YouTube bietet nur verlustbehaftete Audioqualität. Deswegen ist es kein Teil des verlustfreien Fallbacks.';
@override
String get youtubeOpusBitrateTitle => 'YouTube Opus Bitrate';
@@ -1359,13 +1384,13 @@ class AppLocalizationsDe extends AppLocalizations {
String get youtubeMp3BitrateTitle => 'YouTube MP3 Bitrate';
@override
- String get downloadAskBeforeDownload => 'Ask Before Download';
+ String get downloadAskBeforeDownload => 'Qualität vor Download fragen';
@override
- String get downloadDirectory => 'Download Directory';
+ String get downloadDirectory => 'Downloadverzeichnis';
@override
- String get downloadSeparateSinglesFolder => 'Separate Singles Folder';
+ String get downloadSeparateSinglesFolder => 'Singles Ordner trennen';
@override
String get downloadAlbumFolderStructure => 'Album Folder Structure';
@@ -1378,53 +1403,53 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get downloadUsePrimaryArtistOnlyEnabled =>
- 'Featured artists removed from folder name (e.g. Justin Bieber, Quavo → Justin Bieber)';
+ 'Vorgestellte Künstler aus dem Ordnernamen entfernt (z.B. Justin Bieber, Quavo → Justin Bieber)';
@override
String get downloadUsePrimaryArtistOnlyDisabled =>
'Full artist string used for folder name';
@override
- String get downloadSelectQuality => 'Select Quality';
+ String get downloadSelectQuality => 'Qualität wählen';
@override
- String get downloadFrom => 'Download From';
+ String get downloadFrom => 'Herunterladen von';
@override
- String get appearanceAmoledDark => 'AMOLED Dark';
+ String get appearanceAmoledDark => 'AMOLED Schwarz';
@override
- String get appearanceAmoledDarkSubtitle => 'Pure black background';
+ String get appearanceAmoledDarkSubtitle => 'AMOLED Hintergrund';
@override
- String get queueClearAll => 'Clear All';
+ String get queueClearAll => 'Alles löschen';
@override
String get queueClearAllMessage =>
- 'Are you sure you want to clear all downloads?';
+ 'Bist du dir sicher, dass du alle Downloads löschen möchten?';
@override
String get settingsAutoExportFailed => 'Auto-export failed downloads';
@override
String get settingsAutoExportFailedSubtitle =>
- 'Save failed downloads to TXT file automatically';
+ 'Fehlgeschlagene Downloads automatisch in eine TXT-Datei speichern';
@override
- String get settingsDownloadNetwork => 'Download Network';
+ String get settingsDownloadNetwork => 'Download Netzwerk';
@override
- String get settingsDownloadNetworkAny => 'WiFi + Mobile Data';
+ String get settingsDownloadNetworkAny => 'WLAN + Mobile Daten';
@override
- String get settingsDownloadNetworkWifiOnly => 'WiFi Only';
+ String get settingsDownloadNetworkWifiOnly => 'Nur WLAN';
@override
String get settingsDownloadNetworkSubtitle =>
- 'Choose which network to use for downloads. When set to WiFi Only, downloads will pause on mobile data.';
+ 'Wähle aus, welches Netzwerk für Downloads verwendet werden soll. Wenn nur WLAN aktiviert wird, werden Downloads auf mobilen Daten angehalten.';
@override
- String get albumFolderArtistAlbum => 'Artist / Album';
+ String get albumFolderArtistAlbum => 'Künstler/Album';
@override
String get albumFolderArtistAlbumSubtitle => 'Albums/Artist Name/Album Name/';
@@ -1434,13 +1459,13 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get albumFolderArtistYearAlbumSubtitle =>
- 'Albums/Artist Name/[2005] Album Name/';
+ 'Albums/Künster Name/[2005] Album Name/';
@override
- String get albumFolderAlbumOnly => 'Album Only';
+ String get albumFolderAlbumOnly => 'Nur Alben';
@override
- String get albumFolderAlbumOnlySubtitle => 'Albums/Album Name/';
+ String get albumFolderAlbumOnlySubtitle => 'Alben/Album Name/';
@override
String get albumFolderYearAlbum => '[Year] Album';
@@ -1456,39 +1481,39 @@ class AppLocalizationsDe extends AppLocalizations {
'Artist/Album/ and Artist/Singles/';
@override
- String get downloadedAlbumDeleteSelected => 'Delete Selected';
+ String get downloadedAlbumDeleteSelected => 'Ausgewählte löschen';
@override
String downloadedAlbumDeleteMessage(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
- other: 'tracks',
- one: 'track',
+ other: 'Titel',
+ one: 'Titel',
);
- return 'Delete $count $_temp0 from this album?\n\nThis will also delete the files from storage.';
+ return '$count $_temp0 aus diesem Album löschen?\n\nDadurch werden auch die Dateien aus dem Speicher gelöscht.';
}
@override
String downloadedAlbumSelectedCount(int count) {
- return '$count selected';
+ return '$count ausgewählt';
}
@override
- String get downloadedAlbumAllSelected => 'All tracks selected';
+ String get downloadedAlbumAllSelected => 'Alle Titel sind ausgewählt';
@override
- String get downloadedAlbumTapToSelect => 'Tap tracks to select';
+ String get downloadedAlbumTapToSelect => 'Tippe auf Titel zum Auswählen';
@override
String downloadedAlbumDeleteCount(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
- other: 'tracks',
- one: 'track',
+ other: 'Titel',
+ one: 'Titel',
);
- return 'Delete $count $_temp0';
+ return 'Lösche $count $_temp0';
}
@override
@@ -1500,22 +1525,22 @@ class AppLocalizationsDe extends AppLocalizations {
}
@override
- String get recentTypeArtist => 'Artist';
+ String get recentTypeArtist => 'Künstler';
@override
String get recentTypeAlbum => 'Album';
@override
- String get recentTypeSong => 'Song';
+ String get recentTypeSong => 'Titel';
@override
String get recentTypePlaylist => 'Playlist';
@override
- String get recentEmpty => 'No recent items yet';
+ String get recentEmpty => 'Noch keine aktuellen Einträge';
@override
- String get recentShowAllDownloads => 'Show All Downloads';
+ String get recentShowAllDownloads => 'Alle Downloads anzeigen';
@override
String recentPlaylistInfo(String name) {
@@ -1523,41 +1548,41 @@ class AppLocalizationsDe extends AppLocalizations {
}
@override
- String get discographyDownload => 'Download Discography';
+ String get discographyDownload => 'Diskographie herunterladen';
@override
- String get discographyDownloadAll => 'Download All';
+ String get discographyDownloadAll => 'Alle Herunterladen';
@override
String discographyDownloadAllSubtitle(int count, int albumCount) {
- return '$count tracks from $albumCount releases';
+ return '$count Titel von $albumCount Releases';
}
@override
- String get discographyAlbumsOnly => 'Albums Only';
+ String get discographyAlbumsOnly => 'Nur Alben';
@override
String discographyAlbumsOnlySubtitle(int count, int albumCount) {
- return '$count tracks from $albumCount albums';
+ return '$count Titel von $albumCount Albums';
}
@override
- String get discographySinglesOnly => 'Singles & EPs Only';
+ String get discographySinglesOnly => 'Nur Singles & EPs';
@override
String discographySinglesOnlySubtitle(int count, int albumCount) {
- return '$count tracks from $albumCount singles';
+ return '$count Titel von $albumCount Singles';
}
@override
- String get discographySelectAlbums => 'Select Albums...';
+ String get discographySelectAlbums => 'Alben auswählen...';
@override
String get discographySelectAlbumsSubtitle =>
'Choose specific albums or singles';
@override
- String get discographyFetchingTracks => 'Fetching tracks...';
+ String get discographyFetchingTracks => 'Lade Titel...';
@override
String discographyFetchingAlbum(int current, int total) {
@@ -1566,11 +1591,11 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String discographySelectedCount(int count) {
- return '$count selected';
+ return '$count ausgewählt';
}
@override
- String get discographyDownloadSelected => 'Download Selected';
+ String get discographyDownloadSelected => 'Auswahl herunterladen';
@override
String discographyAddedToQueue(int count) {
@@ -1579,20 +1604,20 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String discographySkippedDownloaded(int added, int skipped) {
- return '$added added, $skipped already downloaded';
+ return '$added hinzugefügt, $skipped bereits heruntergeladen';
}
@override
- String get discographyNoAlbums => 'No albums available';
+ String get discographyNoAlbums => 'Es sind keine Alben verfügbar';
@override
String get discographyFailedToFetch => 'Failed to fetch some albums';
@override
- String get sectionStorageAccess => 'Storage Access';
+ String get sectionStorageAccess => 'Speicherzugriff';
@override
- String get allFilesAccess => 'All Files Access';
+ String get allFilesAccess => 'Zugriff auf alle Dateien';
@override
String get allFilesAccessEnabledSubtitle => 'Can write to any folder';
@@ -1602,170 +1627,170 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get allFilesAccessDescription =>
- 'Enable this if you encounter write errors when saving to custom folders. Android 13+ restricts access to certain directories by default.';
+ 'Aktiviere die Option, wenn beim Speichern in benutzerdefinierten Ordnern Schreibfehler auftreten. Weil Android 13+ standardmäßig den Zugriff auf bestimmte Verzeichnisse einschränkt.';
@override
String get allFilesAccessDeniedMessage =>
- 'Permission was denied. Please enable \'All files access\' manually in system settings.';
+ 'Zugriff verweigert. Bitte aktiviere \"Zugriff auf alle Dateien\" manuell in den Systemeinstellungen.';
@override
String get allFilesAccessDisabledMessage =>
- 'All Files Access disabled. The app will use limited storage access.';
+ 'Zugriff auf alle Dateien ist deaktiviert. Die App verwendet nur begrenzten Zugriff auf den Speicher.';
@override
- String get settingsLocalLibrary => 'Local Library';
+ String get settingsLocalLibrary => 'Lokale Bibliothek';
@override
String get settingsLocalLibrarySubtitle => 'Scan music & detect duplicates';
@override
- String get settingsCache => 'Storage & Cache';
+ String get settingsCache => 'Speicher & Cache';
@override
String get settingsCacheSubtitle => 'View size and clear cached data';
@override
- String get libraryTitle => 'Local Library';
+ String get libraryTitle => 'Lokale Bibliothek';
@override
- String get libraryScanSettings => 'Scan Settings';
+ String get libraryScanSettings => 'Scan Einstellungen';
@override
- String get libraryEnableLocalLibrary => 'Enable Local Library';
+ String get libraryEnableLocalLibrary => 'Lokale Bibliothek aktivieren';
@override
String get libraryEnableLocalLibrarySubtitle =>
'Scan and track your existing music';
@override
- String get libraryFolder => 'Library Folder';
+ String get libraryFolder => 'Bibliotheksordner';
@override
- String get libraryFolderHint => 'Tap to select folder';
+ String get libraryFolderHint => 'Tippe um Ordner auszuwählen';
@override
String get libraryShowDuplicateIndicator => 'Show Duplicate Indicator';
@override
String get libraryShowDuplicateIndicatorSubtitle =>
- 'Show when searching for existing tracks';
+ 'Bei der Suche nach vorhandenen Titeln anzeigen';
@override
- String get libraryActions => 'Actions';
+ String get libraryActions => 'Aktionen';
@override
- String get libraryScan => 'Scan Library';
+ String get libraryScan => 'Bibliothek scannen';
@override
- String get libraryScanSubtitle => 'Scan for audio files';
+ String get libraryScanSubtitle => 'Suche nach Audiodateien';
@override
- String get libraryScanSelectFolderFirst => 'Select a folder first';
+ String get libraryScanSelectFolderFirst => 'Wähle zuerst einen Ordner';
@override
- String get libraryCleanupMissingFiles => 'Cleanup Missing Files';
+ String get libraryCleanupMissingFiles => 'Fehlende Dateien bereinigen';
@override
String get libraryCleanupMissingFilesSubtitle =>
- 'Remove entries for files that no longer exist';
+ 'Verlaufseinträge für Dateien löschen, die nicht mehr existieren';
@override
- String get libraryClear => 'Clear Library';
+ String get libraryClear => 'Bibliothek löschen';
@override
- String get libraryClearSubtitle => 'Remove all scanned tracks';
+ String get libraryClearSubtitle => 'Alle gescannten Titel entfernen';
@override
- String get libraryClearConfirmTitle => 'Clear Library';
+ String get libraryClearConfirmTitle => 'Bibliothek löschen';
@override
String get libraryClearConfirmMessage =>
- 'This will remove all scanned tracks from your library. Your actual music files will not be deleted.';
+ 'Dadurch werden alle gescannten Titel aus Ihrer Bibliothek entfernt. Ihre eigentlichen Musikdateien werden nicht gelöscht.';
@override
- String get libraryAbout => 'About Local Library';
+ String get libraryAbout => 'Über die lokale Bibliothek';
@override
String get libraryAboutDescription =>
- 'Scans your existing music collection to detect duplicates when downloading. Supports FLAC, M4A, MP3, Opus, and OGG formats. Metadata is read from file tags when available.';
+ 'Durchsucht deine bestehende Musiksammlung, um Duplikate beim Herunterladen zu erkennen. Unterstützt die Formate FLAC, M4A, MP3, Opus und OGG. Metadaten werden, sofern verfügbar, aus den Dateitags gelesen.';
@override
String libraryTracksUnit(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
- other: 'tracks',
- one: 'track',
+ other: '$count Titel',
+ one: '1 Titel',
);
return '$_temp0';
}
@override
String libraryLastScanned(String time) {
- return 'Last scanned: $time';
+ return 'Zuletzt gescannt: $time';
}
@override
- String get libraryLastScannedNever => 'Never';
+ String get libraryLastScannedNever => 'Nie';
@override
- String get libraryScanning => 'Scanning...';
+ String get libraryScanning => 'Scannen...';
@override
String libraryScanProgress(String progress, int total) {
- return '$progress% of $total files';
+ return '$progress% von $total Dateien';
}
@override
- String get libraryInLibrary => 'In Library';
+ String get libraryInLibrary => 'In Bibliothek';
@override
String libraryRemovedMissingFiles(int count) {
- return 'Removed $count missing files from library';
+ return 'Entfernte $count fehlende Dateien aus der Bibliothek';
}
@override
- String get libraryCleared => 'Library cleared';
+ String get libraryCleared => 'Bibliothek geleert';
@override
- String get libraryStorageAccessRequired => 'Storage Access Required';
+ String get libraryStorageAccessRequired => 'Speicherzugriff erforderlich';
@override
String get libraryStorageAccessMessage =>
- 'SpotiFLAC needs storage access to scan your music library. Please grant permission in settings.';
+ 'SpotiFLAC benötigt Speicherzugriff, um deine Musikbibliothek zu scannen. Bitte erteile die Berechtigung in den Einstellungen.';
@override
- String get libraryFolderNotExist => 'Selected folder does not exist';
+ String get libraryFolderNotExist => 'Der ausgewählte Ordner existiert nicht';
@override
- String get librarySourceDownloaded => 'Downloaded';
+ String get librarySourceDownloaded => 'Heruntergeladen';
@override
- String get librarySourceLocal => 'Local';
+ String get librarySourceLocal => 'Lokal';
@override
- String get libraryFilterAll => 'All';
+ String get libraryFilterAll => 'Alle';
@override
- String get libraryFilterDownloaded => 'Downloaded';
+ String get libraryFilterDownloaded => 'Heruntergeladen';
@override
- String get libraryFilterLocal => 'Local';
+ String get libraryFilterLocal => 'Lokal';
@override
- String get libraryFilterTitle => 'Filters';
+ String get libraryFilterTitle => 'Filter';
@override
- String get libraryFilterReset => 'Reset';
+ String get libraryFilterReset => 'Zurücksetzen';
@override
- String get libraryFilterApply => 'Apply';
+ String get libraryFilterApply => 'Anwenden';
@override
- String get libraryFilterSource => 'Source';
+ String get libraryFilterSource => 'Quelle';
@override
- String get libraryFilterQuality => 'Quality';
+ String get libraryFilterQuality => 'Qualität';
@override
String get libraryFilterQualityHiRes => 'Hi-Res (24bit)';
@@ -1774,30 +1799,30 @@ class AppLocalizationsDe extends AppLocalizations {
String get libraryFilterQualityCD => 'CD (16bit)';
@override
- String get libraryFilterQualityLossy => 'Lossy';
+ String get libraryFilterQualityLossy => 'Verlustbehaftet';
@override
String get libraryFilterFormat => 'Format';
@override
- String get libraryFilterSort => 'Sort';
+ String get libraryFilterSort => 'Sortieren';
@override
- String get libraryFilterSortLatest => 'Latest';
+ String get libraryFilterSortLatest => 'Neuste';
@override
- String get libraryFilterSortOldest => 'Oldest';
+ String get libraryFilterSortOldest => 'Älteste';
@override
- String get timeJustNow => 'Just now';
+ String get timeJustNow => 'Gerade eben';
@override
String timeMinutesAgo(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
- other: '$count minutes ago',
- one: '1 minute ago',
+ other: 'vor $count Minuten',
+ one: 'vor $count Minute',
);
return '$_temp0';
}
@@ -1807,199 +1832,201 @@ class AppLocalizationsDe extends AppLocalizations {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
- other: '$count hours ago',
- one: '1 hour ago',
+ other: 'vor $count Stunden',
+ one: 'vor $count Stunde',
);
return '$_temp0';
}
@override
- String get tutorialWelcomeTitle => 'Welcome to SpotiFLAC!';
+ String get tutorialWelcomeTitle => 'Willkommen bei SpotiFLAC!';
@override
String get tutorialWelcomeDesc =>
- 'Let\'s learn how to download your favorite music in lossless quality. This quick tutorial will show you the basics.';
+ 'Lass uns lernen, wie du deine Lieblingsmusik in verlustfreier Qualität herunterlädst. Dieses schnelle Tutorial zeigt dir die Grundlagen.';
@override
String get tutorialWelcomeTip1 =>
- 'Download music from Spotify, Deezer, or paste any supported URL';
+ 'Lade Musik von Spotify, Deezer herunter oder jeden unterstützten Link einfügen';
@override
String get tutorialWelcomeTip2 =>
- 'Get FLAC quality audio from Tidal, Qobuz, or Amazon Music';
+ 'Hole dir FLAC Audio von Tidal, Qobuz oder Amazon Musik';
@override
String get tutorialWelcomeTip3 =>
- 'Automatic metadata, cover art, and lyrics embedding';
+ 'Automatische Metadaten, Cover und Lyrics einbetten';
@override
- String get tutorialSearchTitle => 'Finding Music';
+ String get tutorialSearchTitle => 'Suche Musik';
@override
String get tutorialSearchDesc =>
- 'There are two easy ways to find music you want to download.';
+ 'Es gibt zwei einfache Möglichkeiten, Musik zu finden, die du herunterladen möchtest.';
@override
- String get tutorialDownloadTitle => 'Downloading Music';
+ String get tutorialDownloadTitle => 'Musik wird heruntergeladen';
@override
String get tutorialDownloadDesc =>
- 'Downloading music is simple and fast. Here\'s how it works.';
+ 'Das Herunterladen von Musik ist einfach und schnell. So funktioniert es.';
@override
- String get tutorialLibraryTitle => 'Your Library';
+ String get tutorialLibraryTitle => 'Deine Bibliothek';
@override
String get tutorialLibraryDesc =>
- 'All your downloaded music is organized in the Library tab.';
+ 'Die gesamte heruntergeladene Musik ist in der Bibliothek organisiert.';
@override
String get tutorialLibraryTip1 =>
- 'View download progress and queue in the Library tab';
+ 'Fortschritt und Warteschlange im Bibliothek‑Tab anzeigen';
@override
String get tutorialLibraryTip2 =>
- 'Tap any track to play it with your music player';
+ 'Tippe auf einen Titel, um ihn mit deinem Musikplayer abzuspielen';
@override
String get tutorialLibraryTip3 =>
- 'Switch between list and grid view for better browsing';
+ 'Wechsle zwischen Listen- und Gitteransicht für ein besseres Surfen';
@override
- String get tutorialExtensionsTitle => 'Extensions';
+ String get tutorialExtensionsTitle => 'Erweiterungen';
@override
String get tutorialExtensionsDesc =>
- 'Extend the app\'s capabilities with community extensions.';
+ 'Erweitere die Fähigkeiten der App mit Community-Erweiterungen.';
@override
String get tutorialExtensionsTip1 =>
- 'Browse the Store tab to discover useful extensions';
+ 'Im Store Tab findest du nützliche Erweiterungen';
@override
String get tutorialExtensionsTip2 =>
- 'Add new download providers or search sources';
+ 'Neue Download- oder Suchanbieter hinzufügen';
@override
String get tutorialExtensionsTip3 =>
- 'Get lyrics, enhanced metadata, and more features';
+ 'Lyrics, erweiterte Metadaten und mehr Funktionen erhalten';
@override
- String get tutorialSettingsTitle => 'Customize Your Experience';
+ String get tutorialSettingsTitle => 'Passe deine Benutzererfahrung an';
@override
String get tutorialSettingsDesc =>
- 'Personalize the app in Settings to match your preferences.';
+ 'Personalisiere die App in den Einstellungen nach deiner Präferenz.';
@override
String get tutorialSettingsTip1 =>
- 'Change download location and folder organization';
+ 'Downloadverzeichnis und Ordnerorganisation ändern';
@override
String get tutorialSettingsTip2 =>
- 'Set default audio quality and format preferences';
+ 'Standard Audioqualität und Formateinstellungen festlegen';
@override
- String get tutorialSettingsTip3 => 'Customize app theme and appearance';
+ String get tutorialSettingsTip3 => 'App-Design und Aussehen anpassen';
@override
String get tutorialReadyMessage =>
- 'You\'re all set! Start downloading your favorite music now.';
+ 'Das ist alles! Lade jetzt deine Lieblingsmusik herunter.';
@override
- String get libraryForceFullScan => 'Force Full Scan';
+ String get libraryForceFullScan => 'Vollen Neu-Scan erzwingen';
@override
- String get libraryForceFullScanSubtitle => 'Rescan all files, ignoring cache';
+ String get libraryForceFullScanSubtitle =>
+ 'Alle Dateien erneut scannen und Cache ignorieren';
@override
- String get cleanupOrphanedDownloads => 'Cleanup Orphaned Downloads';
+ String get cleanupOrphanedDownloads => 'Verwaiste Downloads bereinigen';
@override
String get cleanupOrphanedDownloadsSubtitle =>
- 'Remove history entries for files that no longer exist';
+ 'Verlaufseinträge für Dateien löschen, die nicht mehr existieren';
@override
String cleanupOrphanedDownloadsResult(int count) {
- return 'Removed $count orphaned entries from history';
+ return 'Entfernte $count verwaiste Einträge aus dem Verlauf';
}
@override
- String get cleanupOrphanedDownloadsNone => 'No orphaned entries found';
+ String get cleanupOrphanedDownloadsNone =>
+ 'Keine verwaisten Einträge gefunden';
@override
- String get cacheTitle => 'Storage & Cache';
+ String get cacheTitle => 'Speicher & Cache';
@override
- String get cacheSummaryTitle => 'Cache overview';
+ String get cacheSummaryTitle => 'Cache-Übersicht';
@override
String get cacheSummarySubtitle =>
- 'Clearing cache will not remove downloaded music files.';
+ 'Das Leeren des Caches entfernt nicht heruntergeladene Musikdateien.';
@override
String cacheEstimatedTotal(String size) {
- return 'Estimated cache usage: $size';
+ return 'Geschätzte Cache-Größe: $size';
}
@override
- String get cacheSectionStorage => 'Cached Data';
+ String get cacheSectionStorage => 'Zwischengespeicherte Daten';
@override
- String get cacheSectionMaintenance => 'Maintenance';
+ String get cacheSectionMaintenance => 'Wartung';
@override
- String get cacheAppDirectory => 'App cache directory';
+ String get cacheAppDirectory => 'App-Cache Verzeichnis';
@override
String get cacheAppDirectoryDesc =>
- 'HTTP responses, WebView data, and other temporary app data.';
+ 'HTTP-Antworten, WebView Daten und andere temporäre App-Daten.';
@override
- String get cacheTempDirectory => 'Temporary directory';
+ String get cacheTempDirectory => 'Temporäres Verzeichnis';
@override
String get cacheTempDirectoryDesc =>
- 'Temporary files from downloads and audio conversion.';
+ 'Temporäre Dateien von Downloads und Audio-Konvertierung.';
@override
- String get cacheCoverImage => 'Cover image cache';
+ String get cacheCoverImage => 'Cover-Cache';
@override
String get cacheCoverImageDesc =>
- 'Downloaded album and track cover art. Will re-download when viewed.';
+ 'Album- und Titelcover heruntergeladen. Werden erneut heruntergeladen.';
@override
- String get cacheLibraryCover => 'Library cover cache';
+ String get cacheLibraryCover => 'Bibliotheks-Cover-Cache';
@override
String get cacheLibraryCoverDesc =>
- 'Cover art extracted from local music files. Will re-extract on next scan.';
+ 'Cover aus lokalen Musikdateien extrahiert. Wird beim nächsten Scannen neu extrahiert.';
@override
- String get cacheExploreFeed => 'Explore feed cache';
+ String get cacheExploreFeed => 'Feed-Cache entdecken';
@override
String get cacheExploreFeedDesc =>
- 'Explore tab content (new releases, trending). Will refresh on next visit.';
+ 'Startseiten-Inhalt (neue Releases, Trends). Wird bei einem Neustart aktualisiert.';
@override
- String get cacheTrackLookup => 'Track lookup cache';
+ String get cacheTrackLookup => 'Titel Such-Cache';
@override
String get cacheTrackLookupDesc =>
- 'Spotify/Deezer track ID lookups. Clearing may slow next few searches.';
+ 'Spotify/Deezer Track-ID-Lookups. Das Löschen kann die nächsten Suchergebnisse verlangsamen.';
@override
String get cacheCleanupUnusedDesc =>
- 'Remove orphaned download history and library entries for missing files.';
+ 'Verwaisten Downloadverlauf und Bibliothekseinträge für fehlende Dateien entfernen.';
@override
- String get cacheNoData => 'No cached data';
+ String get cacheNoData => 'Keine gecachten Daten';
@override
String cacheSizeWithFiles(String size, int count) {
- return '$size in $count files';
+ return '$size in $count Dateien';
}
@override
@@ -2009,71 +2036,71 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String cacheEntries(int count) {
- return '$count entries';
+ return '$count Einträge';
}
@override
String cacheClearSuccess(String target) {
- return 'Cleared: $target';
+ return 'Entfernt: $target';
}
@override
- String get cacheClearConfirmTitle => 'Clear cache?';
+ String get cacheClearConfirmTitle => 'Cache leeren?';
@override
String cacheClearConfirmMessage(String target) {
- return 'This will clear cached data for $target. Downloaded music files will not be deleted.';
+ return 'Dies löscht zwischengespeicherte Daten in $target. Die Musikdateien werden nicht gelöscht.';
}
@override
- String get cacheClearAllConfirmTitle => 'Clear all cache?';
+ String get cacheClearAllConfirmTitle => 'Gesamten Cache leeren?';
@override
String get cacheClearAllConfirmMessage =>
- 'This will clear all cache categories on this page. Downloaded music files will not be deleted.';
+ 'Dadurch werden alle Cache-Kategorien auf dieser Seite gelöscht. Heruntergeladene Musikdateien werden nicht gelöscht.';
@override
- String get cacheClearAll => 'Clear all cache';
+ String get cacheClearAll => 'Gesamten Cache leeren';
@override
- String get cacheCleanupUnused => 'Cleanup unused data';
+ String get cacheCleanupUnused => 'Unbenutzte Daten bereinigen';
@override
String get cacheCleanupUnusedSubtitle =>
- 'Remove orphaned download history and missing library entries';
+ 'Verwaisten Downloadverlauf und fehlende Bibliothekseinträge löschen';
@override
String cacheCleanupResult(int downloadCount, int libraryCount) {
- return 'Cleanup completed: $downloadCount orphaned downloads, $libraryCount missing library entries';
+ return 'Bereinigung: $downloadCount verwaiste Downloads, $libraryCount fehlende Bibliothekseinträge';
}
@override
- String get cacheRefreshStats => 'Refresh stats';
+ String get cacheRefreshStats => 'Statistik aktualisieren';
@override
- String get trackSaveCoverArt => 'Save Cover Art';
+ String get trackSaveCoverArt => 'Cover speichern';
@override
- String get trackSaveCoverArtSubtitle => 'Save album art as .jpg file';
+ String get trackSaveCoverArtSubtitle => 'Albumcover als .jpg Datei speichern';
@override
- String get trackSaveLyrics => 'Save Lyrics (.lrc)';
+ String get trackSaveLyrics => 'Lyrics als .lrc speichern';
@override
- String get trackSaveLyricsSubtitle => 'Fetch and save lyrics as .lrc file';
+ String get trackSaveLyricsSubtitle => 'Lade Lyrics als .lrc Datei';
@override
- String get trackSaveLyricsProgress => 'Saving lyrics...';
+ String get trackSaveLyricsProgress => 'Speichere Lyrics...';
@override
- String get trackReEnrich => 'Re-enrich';
+ String get trackReEnrich => 'Neu-anreichern';
@override
String get trackReEnrichOnlineSubtitle =>
- 'Search metadata online and embed into file';
+ 'Metadaten online suchen und in Datei einbinden';
@override
- String get trackEditMetadata => 'Edit Metadata';
+ String get trackEditMetadata => 'Metadaten bearbeiten';
@override
String trackCoverSaved(String fileName) {
@@ -2085,43 +2112,44 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String trackLyricsSaved(String fileName) {
- return 'Lyrics saved to $fileName';
+ return 'Lyrics in $fileName gespeichert';
}
@override
- String get trackReEnrichProgress => 'Re-enriching metadata...';
+ String get trackReEnrichProgress => 'Metadaten neu anreichern...';
@override
- String get trackReEnrichSearching => 'Searching metadata online...';
+ String get trackReEnrichSearching => 'Suche Metadaten online...';
@override
- String get trackReEnrichSuccess => 'Metadata re-enriched successfully';
+ String get trackReEnrichSuccess => 'Metadaten erfolgreich neu angereichert';
@override
- String get trackReEnrichFfmpegFailed => 'FFmpeg metadata embed failed';
+ String get trackReEnrichFfmpegFailed =>
+ 'FFmpeg Metadaten-Einbettung fehlgeschlagen';
@override
String trackSaveFailed(String error) {
- return 'Failed: $error';
+ return 'Fehler: $error';
}
@override
- String get trackConvertFormat => 'Convert Format';
+ String get trackConvertFormat => 'Format konvertieren';
@override
- String get trackConvertFormatSubtitle => 'Convert to MP3 or Opus';
+ String get trackConvertFormatSubtitle => 'In MP3 oder Opus konvertieren';
@override
- String get trackConvertTitle => 'Convert Audio';
+ String get trackConvertTitle => 'Audio konvertieren';
@override
- String get trackConvertTargetFormat => 'Target Format';
+ String get trackConvertTargetFormat => 'Zielformat';
@override
String get trackConvertBitrate => 'Bitrate';
@override
- String get trackConvertConfirmTitle => 'Confirm Conversion';
+ String get trackConvertConfirmTitle => 'Konvertierung bestätigen';
@override
String trackConvertConfirmMessage(
@@ -2129,198 +2157,247 @@ class AppLocalizationsDe extends AppLocalizations {
String targetFormat,
String bitrate,
) {
- return 'Convert from $sourceFormat to $targetFormat at $bitrate?\n\nThe original file will be deleted after conversion.';
+ return 'Konvertieren von $sourceFormat in $targetFormat bei $bitrate?\n\nDie Originaldatei wird nach der Konvertierung gelöscht.';
}
@override
- String get trackConvertConverting => 'Converting audio...';
+ String get trackConvertConverting => 'Konvertiere Audio...';
@override
String trackConvertSuccess(String format) {
- return 'Converted to $format successfully';
+ return 'Konvertiert in $format erfolgreich';
}
@override
- String get trackConvertFailed => 'Conversion failed';
+ String get trackConvertFailed => 'Konvertierung fehlgeschlagen';
@override
- String get actionCreate => 'Create';
+ String get cueSplitTitle => 'Split CUE Sheet';
@override
- String get collectionFoldersTitle => 'My folders';
+ String get cueSplitSubtitle => 'Split CUE+FLAC into individual tracks';
@override
- String get collectionWishlist => 'Wishlist';
+ String cueSplitAlbum(String album) {
+ return 'Album: $album';
+ }
@override
- String get collectionLoved => 'Loved';
+ String cueSplitArtist(String artist) {
+ return 'Artist: $artist';
+ }
@override
- String get collectionPlaylists => 'Playlists';
+ String cueSplitTrackCount(int count) {
+ return '$count tracks';
+ }
+
+ @override
+ String get cueSplitConfirmTitle => 'Split CUE Album';
+
+ @override
+ String cueSplitConfirmMessage(String album, int count) {
+ return 'Split \"$album\" into $count individual FLAC files?\n\nFiles will be saved to the same directory.';
+ }
+
+ @override
+ String cueSplitSplitting(int current, int total) {
+ return 'Splitting CUE sheet... ($current/$total)';
+ }
+
+ @override
+ String cueSplitSuccess(int count) {
+ return 'Split into $count tracks successfully';
+ }
+
+ @override
+ String get cueSplitFailed => 'CUE split failed';
+
+ @override
+ String get cueSplitNoAudioFile => 'Audio file not found for this CUE sheet';
+
+ @override
+ String get cueSplitButton => 'Split into Tracks';
+
+ @override
+ String get actionCreate => 'Erstellen';
+
+ @override
+ String get collectionFoldersTitle => 'Meine Ordner';
+
+ @override
+ String get collectionWishlist => 'Wunschliste';
+
+ @override
+ String get collectionLoved => 'Lieblingssongs';
+
+ @override
+ String get collectionPlaylists => 'Playlisten';
@override
String get collectionPlaylist => 'Playlist';
@override
- String get collectionAddToPlaylist => 'Add to playlist';
+ String get collectionAddToPlaylist => 'Zur Playlist hinzufügen';
@override
- String get collectionCreatePlaylist => 'Create playlist';
+ String get collectionCreatePlaylist => 'Playlist erstellen';
@override
- String get collectionNoPlaylistsYet => 'No playlists yet';
+ String get collectionNoPlaylistsYet => 'Noch keine Playlists';
@override
String get collectionNoPlaylistsSubtitle =>
- 'Create a playlist to start categorizing tracks';
+ 'Playlist erstellen, um Titel zu kategorisieren';
@override
String collectionPlaylistTracks(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
- other: '$count tracks',
- one: '1 track',
+ other: '$count Titel',
+ one: '1 Titel',
);
return '$_temp0';
}
@override
String collectionAddedToPlaylist(String playlistName) {
- return 'Added to \"$playlistName\"';
+ return 'Zu \"$playlistName \" hinzugefügt';
}
@override
String collectionAlreadyInPlaylist(String playlistName) {
- return 'Already in \"$playlistName\"';
+ return 'Bereits in \"$playlistName\"';
}
@override
- String get collectionPlaylistCreated => 'Playlist created';
+ String get collectionPlaylistCreated => 'Playlist erstellt';
@override
- String get collectionPlaylistNameHint => 'Playlist name';
+ String get collectionPlaylistNameHint => 'Playlist-Name';
@override
- String get collectionPlaylistNameRequired => 'Playlist name is required';
+ String get collectionPlaylistNameRequired => 'Playlist-Name ist erforderlich';
@override
- String get collectionRenamePlaylist => 'Rename playlist';
+ String get collectionRenamePlaylist => 'Playlist umbenennen';
@override
- String get collectionDeletePlaylist => 'Delete playlist';
+ String get collectionDeletePlaylist => 'Playlist löschen';
@override
String collectionDeletePlaylistMessage(String playlistName) {
- return 'Delete \"$playlistName\" and all tracks inside it?';
+ return 'Willst du \"$playlistName\" und alle darin enthaltenen Titel löschen?';
}
@override
- String get collectionPlaylistDeleted => 'Playlist deleted';
+ String get collectionPlaylistDeleted => 'Playlist gelöscht';
@override
- String get collectionPlaylistRenamed => 'Playlist renamed';
+ String get collectionPlaylistRenamed => 'Playlist umbenannt';
@override
- String get collectionWishlistEmptyTitle => 'Wishlist is empty';
+ String get collectionWishlistEmptyTitle => 'Wunschliste ist leer';
@override
String get collectionWishlistEmptySubtitle =>
- 'Tap + on tracks to save what you want to download later';
+ 'Tippe auf das + bei den Titeln, um sie zum späteren Herunterladen zu speichern';
@override
- String get collectionLovedEmptyTitle => 'Loved folder is empty';
+ String get collectionLovedEmptyTitle => 'Lieblingssongs sind leer';
@override
String get collectionLovedEmptySubtitle =>
- 'Tap love on tracks to keep your favorites';
+ 'Tippe auf das Herz, um deine Favoriten zu behalten';
@override
- String get collectionPlaylistEmptyTitle => 'Playlist is empty';
+ String get collectionPlaylistEmptyTitle => 'Die Playlist ist leer';
@override
String get collectionPlaylistEmptySubtitle =>
- 'Long-press + on any track to add it here';
+ 'Drücke lange + auf einem beliebigen Titel, um ihn hier hinzuzufügen';
@override
- String get collectionRemoveFromPlaylist => 'Remove from playlist';
+ String get collectionRemoveFromPlaylist => 'Von Playlist entfernen';
@override
- String get collectionRemoveFromFolder => 'Remove from folder';
+ String get collectionRemoveFromFolder => 'Aus Ordner entfernen';
@override
String collectionRemoved(String trackName) {
- return '\"$trackName\" removed';
+ return '\"$trackName\" entfernt';
}
@override
String collectionAddedToLoved(String trackName) {
- return '\"$trackName\" added to Loved';
+ return '\"$trackName\" zu Lieblingssongs hinzugefügt';
}
@override
String collectionRemovedFromLoved(String trackName) {
- return '\"$trackName\" removed from Loved';
+ return '\"$trackName\" aus Lieblingssongs entfernt';
}
@override
String collectionAddedToWishlist(String trackName) {
- return '\"$trackName\" added to Wishlist';
+ return '\"$trackName\" zur Wunschliste hinzugefügt';
}
@override
String collectionRemovedFromWishlist(String trackName) {
- return '\"$trackName\" removed from Wishlist';
+ return '\"$trackName\" aus der Wunschliste entfernt';
}
@override
- String get trackOptionAddToLoved => 'Add to Loved';
+ String get trackOptionAddToLoved => 'Zu Lieblingssongs hinzufügen';
@override
- String get trackOptionRemoveFromLoved => 'Remove from Loved';
+ String get trackOptionRemoveFromLoved => 'Aus Lieblingssongs entfernt';
@override
- String get trackOptionAddToWishlist => 'Add to Wishlist';
+ String get trackOptionAddToWishlist => 'Zur Wunschliste hinzufügen';
@override
- String get trackOptionRemoveFromWishlist => 'Remove from Wishlist';
+ String get trackOptionRemoveFromWishlist => 'Von der Wunschliste entfernen';
@override
- String get collectionPlaylistChangeCover => 'Change cover image';
+ String get collectionPlaylistChangeCover => 'Coverbild ändern';
@override
- String get collectionPlaylistRemoveCover => 'Remove cover image';
+ String get collectionPlaylistRemoveCover => 'Cover entfernen';
@override
String selectionShareCount(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
- other: 'tracks',
- one: 'track',
+ other: 'Titel',
+ one: 'Titel',
);
- return 'Share $count $_temp0';
+ return 'Teile $count $_temp0';
}
@override
- String get selectionShareNoFiles => 'No shareable files found';
+ String get selectionShareNoFiles => 'Keine teilbare Dateien gefunden';
@override
String selectionConvertCount(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
- other: 'tracks',
- one: 'track',
+ other: 'Titel',
+ one: 'Titel',
);
- return 'Convert $count $_temp0';
+ return 'Konvertiere $count $_temp0';
}
@override
- String get selectionConvertNoConvertible => 'No convertible tracks selected';
+ String get selectionConvertNoConvertible =>
+ 'Keine konvertierbare Titel ausgewählt';
@override
- String get selectionBatchConvertConfirmTitle => 'Batch Convert';
+ String get selectionBatchConvertConfirmTitle => 'Batch-Konvertierung';
@override
String selectionBatchConvertConfirmMessage(
@@ -2331,30 +2408,30 @@ class AppLocalizationsDe extends AppLocalizations {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
- other: 'tracks',
- one: 'track',
+ other: 'Titel',
+ one: 'Titel',
);
- return 'Convert $count $_temp0 to $format at $bitrate?\n\nOriginal files will be deleted after conversion.';
+ return 'Konvertiere $count $format $_temp0 zu $bitrate?\n\nOriginaldateien werden nach der Konvertierung gelöscht.';
}
@override
String selectionBatchConvertProgress(int current, int total) {
- return 'Converting $current of $total...';
+ return 'Konvertiere $current von $total...';
}
@override
String selectionBatchConvertSuccess(int success, int total, String format) {
- return 'Converted $success of $total tracks to $format';
+ return '$success von $total Titeln in $format konvertiert';
}
@override
String downloadedAlbumDownloadedCount(int count) {
- return '$count downloaded';
+ return '$count heruntergeladen';
}
@override
String get downloadUseAlbumArtistForFoldersAlbumSubtitle =>
- 'Artist folders use Album Artist when available';
+ 'Künstlerordner verwenden den Album-Interpreten, wenn verfügbar';
@override
String get downloadUseAlbumArtistForFoldersTrackSubtitle =>
diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart
index 8f244a7f..646f9a58 100644
--- a/lib/l10n/app_localizations_en.dart
+++ b/lib/l10n/app_localizations_en.dart
@@ -356,7 +356,7 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get aboutAppDescription =>
- 'Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.';
+ 'Download Spotify tracks in lossless quality from Tidal and Qobuz.';
@override
String get artistAlbums => 'Albums';
@@ -688,6 +688,17 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get errorNoTracksFound => 'No tracks found';
+ @override
+ String get errorUrlNotRecognized => 'Link not recognized';
+
+ @override
+ String get errorUrlNotRecognizedMessage =>
+ 'This link is not supported. Make sure the URL is correct and a compatible extension is installed.';
+
+ @override
+ String get errorUrlFetchFailed =>
+ 'Failed to load content from this link. Please try again.';
+
@override
String errorMissingExtensionSource(String item) {
return 'Cannot load $item: missing extension source';
@@ -761,6 +772,13 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get folderOrganizationNone => 'No organization';
+ @override
+ String get folderOrganizationByPlaylist => 'By Playlist';
+
+ @override
+ String get folderOrganizationByPlaylistSubtitle =>
+ 'Separate folder for each playlist';
+
@override
String get folderOrganizationByArtist => 'By Artist';
@@ -1809,7 +1827,7 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get tutorialWelcomeTip2 =>
- 'Get FLAC quality audio from Tidal, Qobuz, or Amazon Music';
+ 'Get FLAC quality audio from Tidal, Qobuz, or Deezer';
@override
String get tutorialWelcomeTip3 =>
@@ -2126,6 +2144,54 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get trackConvertFailed => 'Conversion failed';
+ @override
+ String get cueSplitTitle => 'Split CUE Sheet';
+
+ @override
+ String get cueSplitSubtitle => 'Split CUE+FLAC into individual tracks';
+
+ @override
+ String cueSplitAlbum(String album) {
+ return 'Album: $album';
+ }
+
+ @override
+ String cueSplitArtist(String artist) {
+ return 'Artist: $artist';
+ }
+
+ @override
+ String cueSplitTrackCount(int count) {
+ return '$count tracks';
+ }
+
+ @override
+ String get cueSplitConfirmTitle => 'Split CUE Album';
+
+ @override
+ String cueSplitConfirmMessage(String album, int count) {
+ return 'Split \"$album\" into $count individual FLAC files?\n\nFiles will be saved to the same directory.';
+ }
+
+ @override
+ String cueSplitSplitting(int current, int total) {
+ return 'Splitting CUE sheet... ($current/$total)';
+ }
+
+ @override
+ String cueSplitSuccess(int count) {
+ return 'Split into $count tracks successfully';
+ }
+
+ @override
+ String get cueSplitFailed => 'CUE split failed';
+
+ @override
+ String get cueSplitNoAudioFile => 'Audio file not found for this CUE sheet';
+
+ @override
+ String get cueSplitButton => 'Split into Tracks';
+
@override
String get actionCreate => 'Create';
diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart
index 58ae25af..e44fa9d8 100644
--- a/lib/l10n/app_localizations_es.dart
+++ b/lib/l10n/app_localizations_es.dart
@@ -356,7 +356,7 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get aboutAppDescription =>
- 'Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.';
+ 'Download Spotify tracks in lossless quality from Tidal and Qobuz.';
@override
String get artistAlbums => 'Albums';
@@ -688,6 +688,17 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get errorNoTracksFound => 'No tracks found';
+ @override
+ String get errorUrlNotRecognized => 'Link not recognized';
+
+ @override
+ String get errorUrlNotRecognizedMessage =>
+ 'This link is not supported. Make sure the URL is correct and a compatible extension is installed.';
+
+ @override
+ String get errorUrlFetchFailed =>
+ 'Failed to load content from this link. Please try again.';
+
@override
String errorMissingExtensionSource(String item) {
return 'Cannot load $item: missing extension source';
@@ -761,6 +772,13 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get folderOrganizationNone => 'No organization';
+ @override
+ String get folderOrganizationByPlaylist => 'By Playlist';
+
+ @override
+ String get folderOrganizationByPlaylistSubtitle =>
+ 'Separate folder for each playlist';
+
@override
String get folderOrganizationByArtist => 'By Artist';
@@ -1809,7 +1827,7 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get tutorialWelcomeTip2 =>
- 'Get FLAC quality audio from Tidal, Qobuz, or Amazon Music';
+ 'Get FLAC quality audio from Tidal, Qobuz, or Deezer';
@override
String get tutorialWelcomeTip3 =>
@@ -2126,6 +2144,54 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get trackConvertFailed => 'Conversion failed';
+ @override
+ String get cueSplitTitle => 'Split CUE Sheet';
+
+ @override
+ String get cueSplitSubtitle => 'Split CUE+FLAC into individual tracks';
+
+ @override
+ String cueSplitAlbum(String album) {
+ return 'Album: $album';
+ }
+
+ @override
+ String cueSplitArtist(String artist) {
+ return 'Artist: $artist';
+ }
+
+ @override
+ String cueSplitTrackCount(int count) {
+ return '$count tracks';
+ }
+
+ @override
+ String get cueSplitConfirmTitle => 'Split CUE Album';
+
+ @override
+ String cueSplitConfirmMessage(String album, int count) {
+ return 'Split \"$album\" into $count individual FLAC files?\n\nFiles will be saved to the same directory.';
+ }
+
+ @override
+ String cueSplitSplitting(int current, int total) {
+ return 'Splitting CUE sheet... ($current/$total)';
+ }
+
+ @override
+ String cueSplitSuccess(int count) {
+ return 'Split into $count tracks successfully';
+ }
+
+ @override
+ String get cueSplitFailed => 'CUE split failed';
+
+ @override
+ String get cueSplitNoAudioFile => 'Audio file not found for this CUE sheet';
+
+ @override
+ String get cueSplitButton => 'Split into Tracks';
+
@override
String get actionCreate => 'Create';
@@ -2705,7 +2771,7 @@ class AppLocalizationsEsEs extends AppLocalizationsEs {
@override
String get aboutAppDescription =>
- 'Descarga pistas de Spotify con calidad sin pérdida de Tidal, Qobuz y Amazon Music.';
+ 'Descarga pistas de Spotify con calidad sin pérdida de Tidal y Qobuz.';
@override
String get artistAlbums => 'Álbumes';
@@ -4150,7 +4216,7 @@ class AppLocalizationsEsEs extends AppLocalizationsEs {
@override
String get tutorialWelcomeTip2 =>
- 'Get FLAC quality audio from Tidal, Qobuz, or Amazon Music';
+ 'Obtén audio en calidad FLAC de Tidal, Qobuz o Deezer';
@override
String get tutorialWelcomeTip3 =>
diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart
index adf0d48d..ae24b7e9 100644
--- a/lib/l10n/app_localizations_fr.dart
+++ b/lib/l10n/app_localizations_fr.dart
@@ -690,6 +690,17 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get errorNoTracksFound => 'No tracks found';
+ @override
+ String get errorUrlNotRecognized => 'Link not recognized';
+
+ @override
+ String get errorUrlNotRecognizedMessage =>
+ 'This link is not supported. Make sure the URL is correct and a compatible extension is installed.';
+
+ @override
+ String get errorUrlFetchFailed =>
+ 'Failed to load content from this link. Please try again.';
+
@override
String errorMissingExtensionSource(String item) {
return 'Cannot load $item: missing extension source';
@@ -763,6 +774,13 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get folderOrganizationNone => 'No organization';
+ @override
+ String get folderOrganizationByPlaylist => 'By Playlist';
+
+ @override
+ String get folderOrganizationByPlaylistSubtitle =>
+ 'Separate folder for each playlist';
+
@override
String get folderOrganizationByArtist => 'By Artist';
@@ -2128,6 +2146,54 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get trackConvertFailed => 'Conversion failed';
+ @override
+ String get cueSplitTitle => 'Split CUE Sheet';
+
+ @override
+ String get cueSplitSubtitle => 'Split CUE+FLAC into individual tracks';
+
+ @override
+ String cueSplitAlbum(String album) {
+ return 'Album: $album';
+ }
+
+ @override
+ String cueSplitArtist(String artist) {
+ return 'Artist: $artist';
+ }
+
+ @override
+ String cueSplitTrackCount(int count) {
+ return '$count tracks';
+ }
+
+ @override
+ String get cueSplitConfirmTitle => 'Split CUE Album';
+
+ @override
+ String cueSplitConfirmMessage(String album, int count) {
+ return 'Split \"$album\" into $count individual FLAC files?\n\nFiles will be saved to the same directory.';
+ }
+
+ @override
+ String cueSplitSplitting(int current, int total) {
+ return 'Splitting CUE sheet... ($current/$total)';
+ }
+
+ @override
+ String cueSplitSuccess(int count) {
+ return 'Split into $count tracks successfully';
+ }
+
+ @override
+ String get cueSplitFailed => 'CUE split failed';
+
+ @override
+ String get cueSplitNoAudioFile => 'Audio file not found for this CUE sheet';
+
+ @override
+ String get cueSplitButton => 'Split into Tracks';
+
@override
String get actionCreate => 'Create';
diff --git a/lib/l10n/app_localizations_hi.dart b/lib/l10n/app_localizations_hi.dart
index 50ca83db..08101eac 100644
--- a/lib/l10n/app_localizations_hi.dart
+++ b/lib/l10n/app_localizations_hi.dart
@@ -688,6 +688,17 @@ class AppLocalizationsHi extends AppLocalizations {
@override
String get errorNoTracksFound => 'No tracks found';
+ @override
+ String get errorUrlNotRecognized => 'Link not recognized';
+
+ @override
+ String get errorUrlNotRecognizedMessage =>
+ 'This link is not supported. Make sure the URL is correct and a compatible extension is installed.';
+
+ @override
+ String get errorUrlFetchFailed =>
+ 'Failed to load content from this link. Please try again.';
+
@override
String errorMissingExtensionSource(String item) {
return 'Cannot load $item: missing extension source';
@@ -761,6 +772,13 @@ class AppLocalizationsHi extends AppLocalizations {
@override
String get folderOrganizationNone => 'No organization';
+ @override
+ String get folderOrganizationByPlaylist => 'By Playlist';
+
+ @override
+ String get folderOrganizationByPlaylistSubtitle =>
+ 'Separate folder for each playlist';
+
@override
String get folderOrganizationByArtist => 'By Artist';
@@ -2126,6 +2144,54 @@ class AppLocalizationsHi extends AppLocalizations {
@override
String get trackConvertFailed => 'Conversion failed';
+ @override
+ String get cueSplitTitle => 'Split CUE Sheet';
+
+ @override
+ String get cueSplitSubtitle => 'Split CUE+FLAC into individual tracks';
+
+ @override
+ String cueSplitAlbum(String album) {
+ return 'Album: $album';
+ }
+
+ @override
+ String cueSplitArtist(String artist) {
+ return 'Artist: $artist';
+ }
+
+ @override
+ String cueSplitTrackCount(int count) {
+ return '$count tracks';
+ }
+
+ @override
+ String get cueSplitConfirmTitle => 'Split CUE Album';
+
+ @override
+ String cueSplitConfirmMessage(String album, int count) {
+ return 'Split \"$album\" into $count individual FLAC files?\n\nFiles will be saved to the same directory.';
+ }
+
+ @override
+ String cueSplitSplitting(int current, int total) {
+ return 'Splitting CUE sheet... ($current/$total)';
+ }
+
+ @override
+ String cueSplitSuccess(int count) {
+ return 'Split into $count tracks successfully';
+ }
+
+ @override
+ String get cueSplitFailed => 'CUE split failed';
+
+ @override
+ String get cueSplitNoAudioFile => 'Audio file not found for this CUE sheet';
+
+ @override
+ String get cueSplitButton => 'Split into Tracks';
+
@override
String get actionCreate => 'Create';
diff --git a/lib/l10n/app_localizations_id.dart b/lib/l10n/app_localizations_id.dart
index ff3af0fa..cab860dd 100644
--- a/lib/l10n/app_localizations_id.dart
+++ b/lib/l10n/app_localizations_id.dart
@@ -15,7 +15,7 @@ class AppLocalizationsId extends AppLocalizations {
String get navHome => 'Beranda';
@override
- String get navLibrary => 'Library';
+ String get navLibrary => 'Pustaka';
@override
String get navSettings => 'Pengaturan';
@@ -45,7 +45,7 @@ class AppLocalizationsId extends AppLocalizations {
String get historyFilterSingles => 'Single';
@override
- String get historySearchHint => 'Search history...';
+ String get historySearchHint => 'Cari riwayat...';
@override
String get settingsTitle => 'Pengaturan';
@@ -104,7 +104,7 @@ class AppLocalizationsId extends AppLocalizations {
String get appearanceHistoryViewList => 'Daftar';
@override
- String get appearanceHistoryViewGrid => 'Grid';
+ String get appearanceHistoryViewGrid => 'Kisi';
@override
String get optionsTitle => 'Opsi';
@@ -126,7 +126,7 @@ class AppLocalizationsId extends AppLocalizations {
'Ketuk Deezer atau Spotify untuk beralih dari ekstensi';
@override
- String get optionsAutoFallback => 'Auto Fallback';
+ String get optionsAutoFallback => 'Cadangan Otomatis';
@override
String get optionsAutoFallbackSubtitle =>
@@ -217,7 +217,7 @@ class AppLocalizationsId extends AppLocalizations {
@override
String optionsSpotifyCredentialsConfigured(String clientId) {
- return 'Client ID: $clientId...';
+ return 'ID Klien: $clientId...';
}
@override
@@ -230,7 +230,7 @@ class AppLocalizationsId extends AppLocalizations {
@override
String get optionsSpotifyDeprecationWarning =>
- 'Spotify search will be deprecated on March 3, 2026 due to Spotify API changes. Please switch to Deezer.';
+ 'Pencarian Spotify akan dihentikan pada 3 Maret 2026 karena perubahan API Spotify. Silakan beralih ke Deezer.';
@override
String get extensionsTitle => 'Ekstensi';
@@ -283,7 +283,7 @@ class AppLocalizationsId extends AppLocalizations {
'Seniman berbakat yang membuat logo aplikasi kita yang indah!';
@override
- String get aboutTranslators => 'Translators';
+ String get aboutTranslators => 'Penerjemah';
@override
String get aboutSpecialThanks => 'Terima Kasih Khusus';
@@ -311,19 +311,19 @@ class AppLocalizationsId extends AppLocalizations {
'Sarankan fitur baru untuk aplikasi';
@override
- String get aboutTelegramChannel => 'Telegram Channel';
+ String get aboutTelegramChannel => 'Saluran Telegram';
@override
- String get aboutTelegramChannelSubtitle => 'Announcements and updates';
+ String get aboutTelegramChannelSubtitle => 'Pengumuman dan pembaruan';
@override
- String get aboutTelegramChat => 'Telegram Community';
+ String get aboutTelegramChat => 'Komunitas Telegram';
@override
- String get aboutTelegramChatSubtitle => 'Chat with other users';
+ String get aboutTelegramChatSubtitle => 'Berbincang dengan pengguna lain';
@override
- String get aboutSocial => 'Social';
+ String get aboutSocial => 'Sosial';
@override
String get aboutApp => 'Aplikasi';
@@ -341,7 +341,7 @@ class AppLocalizationsId extends AppLocalizations {
@override
String get aboutSjdonadoDesc =>
- 'Creator of I Don\'t Have Spotify (IDHS). The fallback link resolver that saves the day!';
+ 'Pencipta I Don\'t Have Spotify (IDHS). Penyelesai tautan cadangan yang menyelamatkan keadaan!';
@override
String get aboutDabMusic => 'DAB Music';
@@ -355,7 +355,7 @@ class AppLocalizationsId extends AppLocalizations {
@override
String get aboutSpotiSaverDesc =>
- 'Tidal Hi-Res FLAC streaming endpoints. A key piece of the lossless puzzle!';
+ 'Tidal perangkat streaming FLAC resolusi tinggi. Bagian penting dari teka-teki tanpa kehilangan kualitas!';
@override
String get aboutAppDescription =>
@@ -456,7 +456,7 @@ class AppLocalizationsId extends AppLocalizations {
@override
String get setupIcloudNotSupported =>
- 'iCloud Drive is not supported. Please use the app Documents folder.';
+ 'iCloud Drive tidak didukung. Silakan gunakan folder Dokumen di aplikasi.';
@override
String get setupDownloadInFlac => 'Unduh lagu Spotify dalam format FLAC';
@@ -593,7 +593,7 @@ class AppLocalizationsId extends AppLocalizations {
@override
String csvImportTracks(int count) {
- return '$count tracks from CSV';
+ return '$count trek dari CSV';
}
@override
@@ -613,7 +613,7 @@ class AppLocalizationsId extends AppLocalizations {
@override
String snackbarAlreadyInLibrary(String trackName) {
- return '\"$trackName\" already exists in your library';
+ return '\"$trackName\" sudah ada di perpustakaan Anda';
}
@override
@@ -691,6 +691,17 @@ class AppLocalizationsId extends AppLocalizations {
@override
String get errorNoTracksFound => 'Tidak ada lagu ditemukan';
+ @override
+ String get errorUrlNotRecognized => 'Link tidak dikenali';
+
+ @override
+ String get errorUrlNotRecognizedMessage =>
+ 'Link ini tidak didukung. Pastikan URL benar dan ekstensi yang kompatibel sudah terpasang.';
+
+ @override
+ String get errorUrlFetchFailed =>
+ 'Gagal memuat konten dari link ini. Silakan coba lagi.';
+
@override
String errorMissingExtensionSource(String item) {
return 'Tidak dapat memuat $item: sumber ekstensi tidak ada';
@@ -755,15 +766,22 @@ class AppLocalizationsId extends AppLocalizations {
String get filenameFormat => 'Format Nama File';
@override
- String get filenameShowAdvancedTags => 'Tampilkan tag lanjutan';
+ String get filenameShowAdvancedTags => 'Show advanced tags';
@override
String get filenameShowAdvancedTagsDescription =>
- 'Aktifkan tag format untuk padding nomor lagu dan pola tanggal';
+ 'Enable formatted tags for track padding and date patterns';
@override
String get folderOrganizationNone => 'Tidak ada';
+ @override
+ String get folderOrganizationByPlaylist => 'By Playlist';
+
+ @override
+ String get folderOrganizationByPlaylistSubtitle =>
+ 'Separate folder for each playlist';
+
@override
String get folderOrganizationByArtist => 'Berdasarkan Artis';
@@ -1343,10 +1361,10 @@ class AppLocalizationsId extends AppLocalizations {
'YouTube provides lossy audio only. Not part of lossless fallback.';
@override
- String get youtubeOpusBitrateTitle => 'Bitrate Opus YouTube';
+ String get youtubeOpusBitrateTitle => 'YouTube Opus Bitrate';
@override
- String get youtubeMp3BitrateTitle => 'Bitrate MP3 YouTube';
+ String get youtubeMp3BitrateTitle => 'YouTube MP3 Bitrate';
@override
String get downloadAskBeforeDownload => 'Tanya Sebelum Unduh';
@@ -1684,8 +1702,8 @@ class AppLocalizationsId extends AppLocalizations {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
- other: 'trek',
- one: 'trek',
+ other: 'tracks',
+ one: 'track',
);
return '$_temp0';
}
@@ -2134,10 +2152,58 @@ class AppLocalizationsId extends AppLocalizations {
String get trackConvertFailed => 'Conversion failed';
@override
- String get actionCreate => 'Buat';
+ String get cueSplitTitle => 'Split CUE Sheet';
@override
- String get collectionFoldersTitle => 'Folder saya';
+ String get cueSplitSubtitle => 'Split CUE+FLAC into individual tracks';
+
+ @override
+ String cueSplitAlbum(String album) {
+ return 'Album: $album';
+ }
+
+ @override
+ String cueSplitArtist(String artist) {
+ return 'Artist: $artist';
+ }
+
+ @override
+ String cueSplitTrackCount(int count) {
+ return '$count tracks';
+ }
+
+ @override
+ String get cueSplitConfirmTitle => 'Split CUE Album';
+
+ @override
+ String cueSplitConfirmMessage(String album, int count) {
+ return 'Split \"$album\" into $count individual FLAC files?\n\nFiles will be saved to the same directory.';
+ }
+
+ @override
+ String cueSplitSplitting(int current, int total) {
+ return 'Splitting CUE sheet... ($current/$total)';
+ }
+
+ @override
+ String cueSplitSuccess(int count) {
+ return 'Split into $count tracks successfully';
+ }
+
+ @override
+ String get cueSplitFailed => 'CUE split failed';
+
+ @override
+ String get cueSplitNoAudioFile => 'Audio file not found for this CUE sheet';
+
+ @override
+ String get cueSplitButton => 'Split into Tracks';
+
+ @override
+ String get actionCreate => 'Create';
+
+ @override
+ String get collectionFoldersTitle => 'My folders';
@override
String get collectionWishlist => 'Wishlist';
@@ -2146,172 +2212,171 @@ class AppLocalizationsId extends AppLocalizations {
String get collectionLoved => 'Loved';
@override
- String get collectionPlaylists => 'Playlist';
+ String get collectionPlaylists => 'Playlists';
@override
String get collectionPlaylist => 'Playlist';
@override
- String get collectionAddToPlaylist => 'Tambahkan ke playlist';
+ String get collectionAddToPlaylist => 'Add to playlist';
@override
- String get collectionCreatePlaylist => 'Buat playlist';
+ String get collectionCreatePlaylist => 'Create playlist';
@override
- String get collectionNoPlaylistsYet => 'Belum ada playlist';
+ String get collectionNoPlaylistsYet => 'No playlists yet';
@override
String get collectionNoPlaylistsSubtitle =>
- 'Buat playlist untuk mulai mengategorikan lagu';
+ 'Create a playlist to start categorizing tracks';
@override
String collectionPlaylistTracks(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
- other: '$count lagu',
- one: '1 lagu',
+ other: '$count tracks',
+ one: '1 track',
);
return '$_temp0';
}
@override
String collectionAddedToPlaylist(String playlistName) {
- return 'Ditambahkan ke \"$playlistName\"';
+ return 'Added to \"$playlistName\"';
}
@override
String collectionAlreadyInPlaylist(String playlistName) {
- return 'Sudah ada di \"$playlistName\"';
+ return 'Already in \"$playlistName\"';
}
@override
- String get collectionPlaylistCreated => 'Playlist berhasil dibuat';
+ String get collectionPlaylistCreated => 'Playlist created';
@override
- String get collectionPlaylistNameHint => 'Nama playlist';
+ String get collectionPlaylistNameHint => 'Playlist name';
@override
- String get collectionPlaylistNameRequired => 'Nama playlist wajib diisi';
+ String get collectionPlaylistNameRequired => 'Playlist name is required';
@override
- String get collectionRenamePlaylist => 'Ubah nama playlist';
+ String get collectionRenamePlaylist => 'Rename playlist';
@override
- String get collectionDeletePlaylist => 'Hapus playlist';
+ String get collectionDeletePlaylist => 'Delete playlist';
@override
String collectionDeletePlaylistMessage(String playlistName) {
- return 'Hapus \"$playlistName\" beserta semua lagunya?';
+ return 'Delete \"$playlistName\" and all tracks inside it?';
}
@override
- String get collectionPlaylistDeleted => 'Playlist dihapus';
+ String get collectionPlaylistDeleted => 'Playlist deleted';
@override
- String get collectionPlaylistRenamed => 'Nama playlist diperbarui';
+ String get collectionPlaylistRenamed => 'Playlist renamed';
@override
- String get collectionWishlistEmptyTitle => 'Wishlist masih kosong';
+ String get collectionWishlistEmptyTitle => 'Wishlist is empty';
@override
String get collectionWishlistEmptySubtitle =>
- 'Tap + di lagu untuk menyimpan yang ingin diunduh nanti';
+ 'Tap + on tracks to save what you want to download later';
@override
- String get collectionLovedEmptyTitle => 'Folder Loved masih kosong';
+ String get collectionLovedEmptyTitle => 'Loved folder is empty';
@override
String get collectionLovedEmptySubtitle =>
- 'Tap love di lagu untuk menyimpan favoritmu';
+ 'Tap love on tracks to keep your favorites';
@override
- String get collectionPlaylistEmptyTitle => 'Playlist masih kosong';
+ String get collectionPlaylistEmptyTitle => 'Playlist is empty';
@override
String get collectionPlaylistEmptySubtitle =>
- 'Tekan lama tombol + pada lagu untuk menambahkannya ke sini';
+ 'Long-press + on any track to add it here';
@override
- String get collectionRemoveFromPlaylist => 'Hapus dari playlist';
+ String get collectionRemoveFromPlaylist => 'Remove from playlist';
@override
- String get collectionRemoveFromFolder => 'Hapus dari folder';
+ String get collectionRemoveFromFolder => 'Remove from folder';
@override
String collectionRemoved(String trackName) {
- return '\"$trackName\" dihapus';
+ return '\"$trackName\" removed';
}
@override
String collectionAddedToLoved(String trackName) {
- return '\"$trackName\" ditambahkan ke Loved';
+ return '\"$trackName\" added to Loved';
}
@override
String collectionRemovedFromLoved(String trackName) {
- return '\"$trackName\" dihapus dari Loved';
+ return '\"$trackName\" removed from Loved';
}
@override
String collectionAddedToWishlist(String trackName) {
- return '\"$trackName\" ditambahkan ke Wishlist';
+ return '\"$trackName\" added to Wishlist';
}
@override
String collectionRemovedFromWishlist(String trackName) {
- return '\"$trackName\" dihapus dari Wishlist';
+ return '\"$trackName\" removed from Wishlist';
}
@override
- String get trackOptionAddToLoved => 'Tambahkan ke Loved';
+ String get trackOptionAddToLoved => 'Add to Loved';
@override
- String get trackOptionRemoveFromLoved => 'Hapus dari Loved';
+ String get trackOptionRemoveFromLoved => 'Remove from Loved';
@override
- String get trackOptionAddToWishlist => 'Tambahkan ke Wishlist';
+ String get trackOptionAddToWishlist => 'Add to Wishlist';
@override
- String get trackOptionRemoveFromWishlist => 'Hapus dari Wishlist';
+ String get trackOptionRemoveFromWishlist => 'Remove from Wishlist';
@override
- String get collectionPlaylistChangeCover => 'Ubah gambar sampul';
+ String get collectionPlaylistChangeCover => 'Change cover image';
@override
- String get collectionPlaylistRemoveCover => 'Hapus gambar sampul';
+ String get collectionPlaylistRemoveCover => 'Remove cover image';
@override
String selectionShareCount(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
- other: 'trek',
- one: 'trek',
+ other: 'tracks',
+ one: 'track',
);
- return 'Bagikan $count $_temp0';
+ return 'Share $count $_temp0';
}
@override
- String get selectionShareNoFiles => 'Tidak ada file yang dapat dibagikan';
+ String get selectionShareNoFiles => 'No shareable files found';
@override
String selectionConvertCount(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
- other: 'trek',
- one: 'trek',
+ other: 'tracks',
+ one: 'track',
);
- return 'Konversi $count $_temp0';
+ return 'Convert $count $_temp0';
}
@override
- String get selectionConvertNoConvertible =>
- 'Tidak ada trek yang dapat dikonversi dipilih';
+ String get selectionConvertNoConvertible => 'No convertible tracks selected';
@override
- String get selectionBatchConvertConfirmTitle => 'Konversi Massal';
+ String get selectionBatchConvertConfirmTitle => 'Batch Convert';
@override
String selectionBatchConvertConfirmMessage(
@@ -2322,20 +2387,20 @@ class AppLocalizationsId extends AppLocalizations {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
- other: 'trek',
- one: 'trek',
+ other: 'tracks',
+ one: 'track',
);
- return 'Konversi $count $_temp0 ke $format pada $bitrate?\n\nFile asli akan dihapus setelah konversi.';
+ return 'Convert $count $_temp0 to $format at $bitrate?\n\nOriginal files will be deleted after conversion.';
}
@override
String selectionBatchConvertProgress(int current, int total) {
- return 'Mengonversi $current dari $total...';
+ return 'Converting $current of $total...';
}
@override
String selectionBatchConvertSuccess(int success, int total, String format) {
- return 'Berhasil mengonversi $success dari $total trek ke $format';
+ return 'Converted $success of $total tracks to $format';
}
@override
diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart
index b473562c..6ce38247 100644
--- a/lib/l10n/app_localizations_ja.dart
+++ b/lib/l10n/app_localizations_ja.dart
@@ -15,7 +15,7 @@ class AppLocalizationsJa extends AppLocalizations {
String get navHome => 'ホーム';
@override
- String get navLibrary => 'Library';
+ String get navLibrary => 'ライブラリ';
@override
String get navSettings => '設定';
@@ -160,7 +160,7 @@ class AppLocalizationsJa extends AppLocalizations {
@override
String optionsConcurrentParallel(int count) {
- return '$count parallel downloads';
+ return '$count 件の分割ダウンロード';
}
@override
@@ -683,6 +683,17 @@ class AppLocalizationsJa extends AppLocalizations {
@override
String get errorNoTracksFound => 'トラックがありません';
+ @override
+ String get errorUrlNotRecognized => 'Link not recognized';
+
+ @override
+ String get errorUrlNotRecognizedMessage =>
+ 'This link is not supported. Make sure the URL is correct and a compatible extension is installed.';
+
+ @override
+ String get errorUrlFetchFailed =>
+ 'Failed to load content from this link. Please try again.';
+
@override
String errorMissingExtensionSource(String item) {
return '$item を読み込めません: 拡張ソースがありません';
@@ -756,6 +767,13 @@ class AppLocalizationsJa extends AppLocalizations {
@override
String get folderOrganizationNone => '構成がありません';
+ @override
+ String get folderOrganizationByPlaylist => 'By Playlist';
+
+ @override
+ String get folderOrganizationByPlaylistSubtitle =>
+ 'Separate folder for each playlist';
+
@override
String get folderOrganizationByArtist => 'アーティスト別';
@@ -1111,7 +1129,7 @@ class AppLocalizationsJa extends AppLocalizations {
String get trackLyricsLoadFailed => '歌詞の読み込みに失敗しました';
@override
- String get trackEmbedLyrics => 'Embed Lyrics';
+ String get trackEmbedLyrics => '歌詞を埋め込む';
@override
String get trackLyricsEmbedded => 'Lyrics embedded successfully';
@@ -1325,10 +1343,10 @@ class AppLocalizationsJa extends AppLocalizations {
'YouTube provides lossy audio only. Not part of lossless fallback.';
@override
- String get youtubeOpusBitrateTitle => 'YouTube Opus Bitrate';
+ String get youtubeOpusBitrateTitle => 'YouTube Opus のビットレート';
@override
- String get youtubeMp3BitrateTitle => 'YouTube MP3 Bitrate';
+ String get youtubeMp3BitrateTitle => 'YouTube MP3 のビットレート';
@override
String get downloadAskBeforeDownload => 'ダウンロード前に確認する';
@@ -1375,20 +1393,20 @@ class AppLocalizationsJa extends AppLocalizations {
String get queueClearAllMessage => 'すべてのダウンロードを消去してもよろしいですか?';
@override
- String get settingsAutoExportFailed => 'Auto-export failed downloads';
+ String get settingsAutoExportFailed => 'ダウンロードの自動エクスポートに失敗しました';
@override
String get settingsAutoExportFailedSubtitle =>
'Save failed downloads to TXT file automatically';
@override
- String get settingsDownloadNetwork => 'Download Network';
+ String get settingsDownloadNetwork => 'ダウンロードネットワーク';
@override
- String get settingsDownloadNetworkAny => 'WiFi + Mobile Data';
+ String get settingsDownloadNetworkAny => 'Wi-Fi + モバイルデータ';
@override
- String get settingsDownloadNetworkWifiOnly => 'WiFi Only';
+ String get settingsDownloadNetworkWifiOnly => 'Wi-Fi のみ';
@override
String get settingsDownloadNetworkSubtitle =>
@@ -1419,7 +1437,7 @@ class AppLocalizationsJa extends AppLocalizations {
String get albumFolderYearAlbumSubtitle => 'アルバム/[2005] アルバム名/';
@override
- String get albumFolderArtistAlbumSingles => 'Artist / Album + Singles';
+ String get albumFolderArtistAlbumSingles => 'アーティスト / アルバム + シングル';
@override
String get albumFolderArtistAlbumSinglesSubtitle =>
@@ -1485,7 +1503,7 @@ class AppLocalizationsJa extends AppLocalizations {
String get recentEmpty => 'No recent items yet';
@override
- String get recentShowAllDownloads => 'Show All Downloads';
+ String get recentShowAllDownloads => 'すべてのダウンロードを表示';
@override
String recentPlaylistInfo(String name) {
@@ -1559,10 +1577,10 @@ class AppLocalizationsJa extends AppLocalizations {
String get discographyFailedToFetch => '一部のアルバムの取得に失敗しました';
@override
- String get sectionStorageAccess => 'Storage Access';
+ String get sectionStorageAccess => 'ストレージアクセス';
@override
- String get allFilesAccess => 'All Files Access';
+ String get allFilesAccess => 'すべてのファイルへのアクセス';
@override
String get allFilesAccessEnabledSubtitle => 'Can write to any folder';
@@ -1583,35 +1601,35 @@ class AppLocalizationsJa extends AppLocalizations {
'All Files Access disabled. The app will use limited storage access.';
@override
- String get settingsLocalLibrary => 'Local Library';
+ String get settingsLocalLibrary => 'ローカルライブラリ';
@override
String get settingsLocalLibrarySubtitle => 'Scan music & detect duplicates';
@override
- String get settingsCache => 'Storage & Cache';
+ String get settingsCache => 'ストレージとキャッシュ';
@override
String get settingsCacheSubtitle => 'View size and clear cached data';
@override
- String get libraryTitle => 'Local Library';
+ String get libraryTitle => 'ローカルライブラリ';
@override
- String get libraryScanSettings => 'Scan Settings';
+ String get libraryScanSettings => 'スキャン設定';
@override
- String get libraryEnableLocalLibrary => 'Enable Local Library';
+ String get libraryEnableLocalLibrary => 'ローカルライブラリを有効';
@override
String get libraryEnableLocalLibrarySubtitle =>
'Scan and track your existing music';
@override
- String get libraryFolder => 'Library Folder';
+ String get libraryFolder => 'ライブラリのフォルダ';
@override
- String get libraryFolderHint => 'Tap to select folder';
+ String get libraryFolderHint => 'タップでフォルダを選択';
@override
String get libraryShowDuplicateIndicator => 'Show Duplicate Indicator';
@@ -1621,13 +1639,13 @@ class AppLocalizationsJa extends AppLocalizations {
'Show when searching for existing tracks';
@override
- String get libraryActions => 'Actions';
+ String get libraryActions => 'アクション';
@override
- String get libraryScan => 'Scan Library';
+ String get libraryScan => 'ライブラリをスキャン';
@override
- String get libraryScanSubtitle => 'Scan for audio files';
+ String get libraryScanSubtitle => 'オーディオファイルをスキャン';
@override
String get libraryScanSelectFolderFirst => 'Select a folder first';
@@ -1640,20 +1658,20 @@ class AppLocalizationsJa extends AppLocalizations {
'Remove entries for files that no longer exist';
@override
- String get libraryClear => 'Clear Library';
+ String get libraryClear => 'ライブラリを消去';
@override
String get libraryClearSubtitle => 'Remove all scanned tracks';
@override
- String get libraryClearConfirmTitle => 'Clear Library';
+ String get libraryClearConfirmTitle => 'ライブラリを消去';
@override
String get libraryClearConfirmMessage =>
'This will remove all scanned tracks from your library. Your actual music files will not be deleted.';
@override
- String get libraryAbout => 'About Local Library';
+ String get libraryAbout => 'ローカルライブラリについて';
@override
String get libraryAboutDescription =>
@@ -1672,14 +1690,14 @@ class AppLocalizationsJa extends AppLocalizations {
@override
String libraryLastScanned(String time) {
- return 'Last scanned: $time';
+ return '最終スキャン: $time';
}
@override
String get libraryLastScannedNever => 'Never';
@override
- String get libraryScanning => 'Scanning...';
+ String get libraryScanning => 'スキャン中...';
@override
String libraryScanProgress(String progress, int total) {
@@ -1687,7 +1705,7 @@ class AppLocalizationsJa extends AppLocalizations {
}
@override
- String get libraryInLibrary => 'In Library';
+ String get libraryInLibrary => 'ライブラリ内';
@override
String libraryRemovedMissingFiles(int count) {
@@ -1698,7 +1716,7 @@ class AppLocalizationsJa extends AppLocalizations {
String get libraryCleared => 'Library cleared';
@override
- String get libraryStorageAccessRequired => 'Storage Access Required';
+ String get libraryStorageAccessRequired => 'ストレージアクセスが必要です';
@override
String get libraryStorageAccessMessage =>
@@ -1708,37 +1726,37 @@ class AppLocalizationsJa extends AppLocalizations {
String get libraryFolderNotExist => 'Selected folder does not exist';
@override
- String get librarySourceDownloaded => 'Downloaded';
+ String get librarySourceDownloaded => 'ダウンロード済み';
@override
- String get librarySourceLocal => 'Local';
+ String get librarySourceLocal => 'ローカル';
@override
- String get libraryFilterAll => 'All';
+ String get libraryFilterAll => 'すべて';
@override
- String get libraryFilterDownloaded => 'Downloaded';
+ String get libraryFilterDownloaded => 'ダウンロード済み';
@override
- String get libraryFilterLocal => 'Local';
+ String get libraryFilterLocal => 'ローカル';
@override
- String get libraryFilterTitle => 'Filters';
+ String get libraryFilterTitle => 'フィルター';
@override
- String get libraryFilterReset => 'Reset';
+ String get libraryFilterReset => 'リセット';
@override
- String get libraryFilterApply => 'Apply';
+ String get libraryFilterApply => '適用';
@override
- String get libraryFilterSource => 'Source';
+ String get libraryFilterSource => 'ソース';
@override
- String get libraryFilterQuality => 'Quality';
+ String get libraryFilterQuality => '品質';
@override
- String get libraryFilterQualityHiRes => 'Hi-Res (24bit)';
+ String get libraryFilterQualityHiRes => 'ハイレゾ (24bit)';
@override
String get libraryFilterQualityCD => 'CD (16bit)';
@@ -1747,7 +1765,7 @@ class AppLocalizationsJa extends AppLocalizations {
String get libraryFilterQualityLossy => 'Lossy';
@override
- String get libraryFilterFormat => 'Format';
+ String get libraryFilterFormat => '形式';
@override
String get libraryFilterSort => 'Sort';
@@ -1766,8 +1784,8 @@ class AppLocalizationsJa extends AppLocalizations {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
- other: '$count minutes ago',
- one: '1 minute ago',
+ other: '$count 分前',
+ one: '1 分前',
);
return '$_temp0';
}
@@ -1777,14 +1795,14 @@ class AppLocalizationsJa extends AppLocalizations {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
- other: '$count hours ago',
- one: '1 hour ago',
+ other: '$count 時間前',
+ one: '1 時間前',
);
return '$_temp0';
}
@override
- String get tutorialWelcomeTitle => 'Welcome to SpotiFLAC!';
+ String get tutorialWelcomeTitle => 'SpotiFLAC へようこそ!';
@override
String get tutorialWelcomeDesc =>
@@ -1810,14 +1828,14 @@ class AppLocalizationsJa extends AppLocalizations {
'There are two easy ways to find music you want to download.';
@override
- String get tutorialDownloadTitle => 'Downloading Music';
+ String get tutorialDownloadTitle => '音楽をダウンロード中';
@override
String get tutorialDownloadDesc =>
'Downloading music is simple and fast. Here\'s how it works.';
@override
- String get tutorialLibraryTitle => 'Your Library';
+ String get tutorialLibraryTitle => 'あなたのライブラリ';
@override
String get tutorialLibraryDesc =>
@@ -1836,7 +1854,7 @@ class AppLocalizationsJa extends AppLocalizations {
'Switch between list and grid view for better browsing';
@override
- String get tutorialExtensionsTitle => 'Extensions';
+ String get tutorialExtensionsTitle => '拡張';
@override
String get tutorialExtensionsDesc =>
@@ -1877,7 +1895,7 @@ class AppLocalizationsJa extends AppLocalizations {
'You\'re all set! Start downloading your favorite music now.';
@override
- String get libraryForceFullScan => 'Force Full Scan';
+ String get libraryForceFullScan => '強制フルスキャン';
@override
String get libraryForceFullScanSubtitle => 'Rescan all files, ignoring cache';
@@ -1898,10 +1916,10 @@ class AppLocalizationsJa extends AppLocalizations {
String get cleanupOrphanedDownloadsNone => 'No orphaned entries found';
@override
- String get cacheTitle => 'Storage & Cache';
+ String get cacheTitle => 'ストレージとキャッシュ';
@override
- String get cacheSummaryTitle => 'Cache overview';
+ String get cacheSummaryTitle => 'キャッシュの概要';
@override
String get cacheSummarySubtitle =>
@@ -1913,34 +1931,34 @@ class AppLocalizationsJa extends AppLocalizations {
}
@override
- String get cacheSectionStorage => 'Cached Data';
+ String get cacheSectionStorage => 'キャッシュ済みデータ';
@override
- String get cacheSectionMaintenance => 'Maintenance';
+ String get cacheSectionMaintenance => 'メンテナンス';
@override
- String get cacheAppDirectory => 'App cache directory';
+ String get cacheAppDirectory => 'アプリキャッシュのディレクトリ';
@override
String get cacheAppDirectoryDesc =>
'HTTP responses, WebView data, and other temporary app data.';
@override
- String get cacheTempDirectory => 'Temporary directory';
+ String get cacheTempDirectory => '一時ディレクトリ';
@override
String get cacheTempDirectoryDesc =>
'Temporary files from downloads and audio conversion.';
@override
- String get cacheCoverImage => 'Cover image cache';
+ String get cacheCoverImage => 'カバー画像のキャッシュ';
@override
String get cacheCoverImageDesc =>
'Downloaded album and track cover art. Will re-download when viewed.';
@override
- String get cacheLibraryCover => 'Library cover cache';
+ String get cacheLibraryCover => 'ライブラリのカバーキャッシュ';
@override
String get cacheLibraryCoverDesc =>
@@ -1965,7 +1983,7 @@ class AppLocalizationsJa extends AppLocalizations {
'Remove orphaned download history and library entries for missing files.';
@override
- String get cacheNoData => 'No cached data';
+ String get cacheNoData => 'キャッシュデータはありません';
@override
String cacheSizeWithFiles(String size, int count) {
@@ -1979,16 +1997,16 @@ class AppLocalizationsJa extends AppLocalizations {
@override
String cacheEntries(int count) {
- return '$count entries';
+ return '$count 個のエントリ';
}
@override
String cacheClearSuccess(String target) {
- return 'Cleared: $target';
+ return '消去済み: $target';
}
@override
- String get cacheClearConfirmTitle => 'Clear cache?';
+ String get cacheClearConfirmTitle => 'キャッシュを消去しますか?';
@override
String cacheClearConfirmMessage(String target) {
@@ -1996,17 +2014,17 @@ class AppLocalizationsJa extends AppLocalizations {
}
@override
- String get cacheClearAllConfirmTitle => 'Clear all cache?';
+ String get cacheClearAllConfirmTitle => 'すべてのキャッシュを消去しますか?';
@override
String get cacheClearAllConfirmMessage =>
'This will clear all cache categories on this page. Downloaded music files will not be deleted.';
@override
- String get cacheClearAll => 'Clear all cache';
+ String get cacheClearAll => 'すべてのキャッシュを消去';
@override
- String get cacheCleanupUnused => 'Cleanup unused data';
+ String get cacheCleanupUnused => '未使用のデータを削除';
@override
String get cacheCleanupUnusedSubtitle =>
@@ -2018,16 +2036,16 @@ class AppLocalizationsJa extends AppLocalizations {
}
@override
- String get cacheRefreshStats => 'Refresh stats';
+ String get cacheRefreshStats => '状態を更新';
@override
- String get trackSaveCoverArt => 'Save Cover Art';
+ String get trackSaveCoverArt => 'カバー画像を保存';
@override
String get trackSaveCoverArtSubtitle => 'Save album art as .jpg file';
@override
- String get trackSaveLyrics => 'Save Lyrics (.lrc)';
+ String get trackSaveLyrics => '歌詞を保存 (.lrc)';
@override
String get trackSaveLyricsSubtitle => 'Fetch and save lyrics as .lrc file';
@@ -2043,7 +2061,7 @@ class AppLocalizationsJa extends AppLocalizations {
'Search metadata online and embed into file';
@override
- String get trackEditMetadata => 'Edit Metadata';
+ String get trackEditMetadata => 'メタデータを編集';
@override
String trackCoverSaved(String fileName) {
@@ -2072,26 +2090,26 @@ class AppLocalizationsJa extends AppLocalizations {
@override
String trackSaveFailed(String error) {
- return 'Failed: $error';
+ return '失敗: $error';
}
@override
- String get trackConvertFormat => 'Convert Format';
+ String get trackConvertFormat => '変換の形式';
@override
- String get trackConvertFormatSubtitle => 'Convert to MP3 or Opus';
+ String get trackConvertFormatSubtitle => 'MP3 または Opus に変換';
@override
- String get trackConvertTitle => 'Convert Audio';
+ String get trackConvertTitle => 'オーディオを変換';
@override
- String get trackConvertTargetFormat => 'Target Format';
+ String get trackConvertTargetFormat => 'ターゲットの形式';
@override
- String get trackConvertBitrate => 'Bitrate';
+ String get trackConvertBitrate => 'ビットレート';
@override
- String get trackConvertConfirmTitle => 'Confirm Conversion';
+ String get trackConvertConfirmTitle => '変換を確認';
@override
String trackConvertConfirmMessage(
@@ -2103,7 +2121,7 @@ class AppLocalizationsJa extends AppLocalizations {
}
@override
- String get trackConvertConverting => 'Converting audio...';
+ String get trackConvertConverting => 'オーディオを変換中...';
@override
String trackConvertSuccess(String format) {
@@ -2111,7 +2129,55 @@ class AppLocalizationsJa extends AppLocalizations {
}
@override
- String get trackConvertFailed => 'Conversion failed';
+ String get trackConvertFailed => '変換に失敗しました';
+
+ @override
+ String get cueSplitTitle => 'Split CUE Sheet';
+
+ @override
+ String get cueSplitSubtitle => 'Split CUE+FLAC into individual tracks';
+
+ @override
+ String cueSplitAlbum(String album) {
+ return 'Album: $album';
+ }
+
+ @override
+ String cueSplitArtist(String artist) {
+ return 'Artist: $artist';
+ }
+
+ @override
+ String cueSplitTrackCount(int count) {
+ return '$count tracks';
+ }
+
+ @override
+ String get cueSplitConfirmTitle => 'Split CUE Album';
+
+ @override
+ String cueSplitConfirmMessage(String album, int count) {
+ return 'Split \"$album\" into $count individual FLAC files?\n\nFiles will be saved to the same directory.';
+ }
+
+ @override
+ String cueSplitSplitting(int current, int total) {
+ return 'Splitting CUE sheet... ($current/$total)';
+ }
+
+ @override
+ String cueSplitSuccess(int count) {
+ return 'Split into $count tracks successfully';
+ }
+
+ @override
+ String get cueSplitFailed => 'CUE split failed';
+
+ @override
+ String get cueSplitNoAudioFile => 'Audio file not found for this CUE sheet';
+
+ @override
+ String get cueSplitButton => 'Split into Tracks';
@override
String get actionCreate => 'Create';
diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart
index f94d1f3f..56287787 100644
--- a/lib/l10n/app_localizations_ko.dart
+++ b/lib/l10n/app_localizations_ko.dart
@@ -45,41 +45,40 @@ class AppLocalizationsKo extends AppLocalizations {
String get historyFilterSingles => 'Singles';
@override
- String get historySearchHint => 'Search history...';
+ String get historySearchHint => '검색 기록...';
@override
String get settingsTitle => 'Settings';
@override
- String get settingsDownload => 'Download';
+ String get settingsDownload => '다운로드';
@override
- String get settingsAppearance => 'Appearance';
+ String get settingsAppearance => '외관';
@override
- String get settingsOptions => 'Options';
+ String get settingsOptions => '옵션';
@override
- String get settingsExtensions => 'Extensions';
+ String get settingsExtensions => '확장 기능';
@override
- String get settingsAbout => 'About';
+ String get settingsAbout => '정보';
@override
- String get downloadTitle => 'Download';
+ String get downloadTitle => '다운로드';
@override
- String get downloadAskQualitySubtitle =>
- 'Show quality picker for each download';
+ String get downloadAskQualitySubtitle => '다운로드를 할 때마다 품질을 선택하도록 합니다';
@override
- String get downloadFilenameFormat => 'Filename Format';
+ String get downloadFilenameFormat => '파일 이름 형식';
@override
- String get downloadFolderOrganization => 'Folder Organization';
+ String get downloadFolderOrganization => '폴더 분류 형식';
@override
- String get appearanceTitle => 'Appearance';
+ String get appearanceTitle => '외관';
@override
String get appearanceThemeSystem => 'System';
@@ -94,10 +93,10 @@ class AppLocalizationsKo extends AppLocalizations {
String get appearanceDynamicColor => 'Dynamic Color';
@override
- String get appearanceDynamicColorSubtitle => 'Use colors from your wallpaper';
+ String get appearanceDynamicColorSubtitle => '배경 화면을 참고하여 강조 색상이 지정됩니다';
@override
- String get appearanceHistoryView => 'History View';
+ String get appearanceHistoryView => '기록 정렬 방식';
@override
String get appearanceHistoryViewList => 'List';
@@ -106,112 +105,104 @@ class AppLocalizationsKo extends AppLocalizations {
String get appearanceHistoryViewGrid => 'Grid';
@override
- String get optionsTitle => 'Options';
+ String get optionsTitle => '옵션';
@override
- String get optionsPrimaryProvider => 'Primary Provider';
+ String get optionsPrimaryProvider => '기본 제공자';
@override
- String get optionsPrimaryProviderSubtitle =>
- 'Service used when searching by track name.';
+ String get optionsPrimaryProviderSubtitle => '음반 이름으로 검색할 때 사용되는 서비스';
@override
String optionsUsingExtension(String extensionName) {
- return 'Using extension: $extensionName';
+ return '확장 기능을 사용: $extensionName';
}
@override
- String get optionsSwitchBack =>
- 'Tap Deezer or Spotify to switch back from extension';
+ String get optionsSwitchBack => 'Deezer 또는 Spotify를 탭하여 확장 기능에서 다시 전환하세요.';
@override
- String get optionsAutoFallback => 'Auto Fallback';
+ String get optionsAutoFallback => '자동 재시도';
@override
String get optionsAutoFallbackSubtitle => '다운로드가 실패한 경우, 다른 서비스로 재시도';
@override
- String get optionsUseExtensionProviders => 'Use Extension Providers';
+ String get optionsUseExtensionProviders => '확장 기능 사용';
@override
String get optionsUseExtensionProvidersOn => '확장 기능을 우선적으로 사용합니다';
@override
- String get optionsUseExtensionProvidersOff => 'Using built-in providers only';
+ String get optionsUseExtensionProvidersOff => '기본으로 제공되는 기능만 사용';
@override
- String get optionsEmbedLyrics => 'Embed Lyrics';
+ String get optionsEmbedLyrics => '가사 삽입';
@override
- String get optionsEmbedLyricsSubtitle =>
- 'Embed synced lyrics into FLAC files';
+ String get optionsEmbedLyricsSubtitle => 'FLAC 파일에 동기화된 가사를 삽입합니다';
@override
- String get optionsMaxQualityCover => 'Max Quality Cover';
+ String get optionsMaxQualityCover => '고품질 커버 이미지';
@override
- String get optionsMaxQualityCoverSubtitle =>
- 'Download highest resolution cover art';
+ String get optionsMaxQualityCoverSubtitle => '최고 품질의 커버 이미지를 다운로드';
@override
- String get optionsConcurrentDownloads => 'Concurrent Downloads';
+ String get optionsConcurrentDownloads => '동시 다운로드';
@override
- String get optionsConcurrentSequential => 'Sequential (1 at a time)';
+ String get optionsConcurrentSequential => '순차 다운로드 (한 번에 하나)';
@override
String optionsConcurrentParallel(int count) {
- return '$count parallel downloads';
+ return '$count개 동시 다운로드';
}
@override
- String get optionsConcurrentWarning =>
- 'Parallel downloads may trigger rate limiting';
+ String get optionsConcurrentWarning => '동시에 다수의 음반을 다운로드하면 속도 제한이 발생할 수 있습니다';
@override
- String get optionsExtensionStore => 'Extension Store';
+ String get optionsExtensionStore => '확장 기능 스토어';
@override
- String get optionsExtensionStoreSubtitle => 'Show Store tab in navigation';
+ String get optionsExtensionStoreSubtitle => '탐색 메뉴에 스토어 탭 표시';
@override
- String get optionsCheckUpdates => 'Check for Updates';
+ String get optionsCheckUpdates => '업데이트 확인';
@override
- String get optionsCheckUpdatesSubtitle =>
- 'Notify when new version is available';
+ String get optionsCheckUpdatesSubtitle => '새로운 버전이 출시되면 알림';
@override
- String get optionsUpdateChannel => 'Update Channel';
+ String get optionsUpdateChannel => '업데이트 채널';
@override
- String get optionsUpdateChannelStable => 'Stable releases only';
+ String get optionsUpdateChannelStable => '안정적인 버전만 수령';
@override
- String get optionsUpdateChannelPreview => 'Get preview releases';
+ String get optionsUpdateChannelPreview => '미리보기 버전을 수령';
@override
- String get optionsUpdateChannelWarning =>
- 'Preview may contain bugs or incomplete features';
+ String get optionsUpdateChannelWarning => '미리보기 버전은 불안정할 수 있습니다';
@override
- String get optionsClearHistory => 'Clear Download History';
+ String get optionsClearHistory => '다운로드 기록 삭제';
@override
- String get optionsClearHistorySubtitle =>
- 'Remove all downloaded tracks from history';
+ String get optionsClearHistorySubtitle => '기록에서 모든 다운로드 음반을 제거합니다';
@override
- String get optionsDetailedLogging => 'Detailed Logging';
+ String get optionsDetailedLogging => '상세 로깅';
@override
- String get optionsDetailedLoggingOn => 'Detailed logs are being recorded';
+ String get optionsDetailedLoggingOn => '상세한 로그가 기록되고 있습니다';
@override
- String get optionsDetailedLoggingOff => 'Enable for bug reports';
+ String get optionsDetailedLoggingOff => '버그 신고를 위한 기능입니다';
@override
- String get optionsSpotifyCredentials => 'Spotify Credentials';
+ String get optionsSpotifyCredentials => 'Spotify 자격 증명';
@override
String optionsSpotifyCredentialsConfigured(String clientId) {
@@ -219,21 +210,21 @@ class AppLocalizationsKo extends AppLocalizations {
}
@override
- String get optionsSpotifyCredentialsRequired => 'Required - tap to configure';
+ String get optionsSpotifyCredentialsRequired => '탭하여 설정';
@override
String get optionsSpotifyWarning =>
- 'Spotify requires your own API credentials. Get them free from developer.spotify.com';
+ 'Spotify는 사용자 고유의 API 자격 증명을 요구합니다. developer.spotify.com에서 무료로 발급받으세요';
@override
String get optionsSpotifyDeprecationWarning =>
- 'Spotify search will be deprecated on March 3, 2026 due to Spotify API changes. Please switch to Deezer.';
+ 'Spotify API 변경으로 인해 Spotify 검색 기능은 2026년 3월 3일부터 더 이상 지원되지 않습니다. Deezer로 전환해 주세요';
@override
- String get extensionsTitle => 'Extensions';
+ String get extensionsTitle => '확장 기능';
@override
- String get extensionsDisabled => 'Disabled';
+ String get extensionsDisabled => '비활성화';
@override
String extensionsVersion(String version) {
@@ -246,80 +237,79 @@ class AppLocalizationsKo extends AppLocalizations {
}
@override
- String get extensionsUninstall => 'Uninstall';
+ String get extensionsUninstall => '삭제';
@override
- String get storeTitle => 'Extension Store';
+ String get storeTitle => '확장 기능 스토어';
@override
- String get storeSearch => 'Search extensions...';
+ String get storeSearch => '확장 기능 검색';
@override
- String get storeInstall => 'Install';
+ String get storeInstall => '설치';
@override
- String get storeInstalled => 'Installed';
+ String get storeInstalled => '설치됨';
@override
- String get storeUpdate => 'Update';
+ String get storeUpdate => '업데이트';
@override
- String get aboutTitle => 'About';
+ String get aboutTitle => '정보';
@override
- String get aboutContributors => 'Contributors';
+ String get aboutContributors => '기여자';
@override
- String get aboutMobileDeveloper => 'Mobile version developer';
+ String get aboutMobileDeveloper => '모바일 버전 개발자';
@override
- String get aboutOriginalCreator => 'Creator of the original SpotiFLAC';
+ String get aboutOriginalCreator => '오리지널 SpotiFLAC 제작자';
@override
- String get aboutLogoArtist =>
- 'The talented artist who created our beautiful app logo!';
+ String get aboutLogoArtist => '아름다운 로고를 만들어주신 재능 있는 아티스트!';
@override
- String get aboutTranslators => 'Translators';
+ String get aboutTranslators => '번역가들';
@override
- String get aboutSpecialThanks => 'Special Thanks';
+ String get aboutSpecialThanks => '특별 감사';
@override
- String get aboutLinks => 'Links';
+ String get aboutLinks => '바로가기';
@override
String get aboutMobileSource => 'Mobile source code';
@override
- String get aboutPCSource => 'PC source code';
+ String get aboutPCSource => 'PC 소스 코드';
@override
- String get aboutReportIssue => 'Report an issue';
+ String get aboutReportIssue => '문제 신고';
@override
- String get aboutReportIssueSubtitle => 'Report any problems you encounter';
+ String get aboutReportIssueSubtitle => '발생하는 모든 문제를 신고하여 주세요.';
@override
- String get aboutFeatureRequest => 'Feature request';
+ String get aboutFeatureRequest => '기능 요청';
@override
- String get aboutFeatureRequestSubtitle => 'Suggest new features for the app';
+ String get aboutFeatureRequestSubtitle => '앱의 새로운 기능을 제안하여 주세요.';
@override
String get aboutTelegramChannel => 'Telegram Channel';
@override
- String get aboutTelegramChannelSubtitle => 'Announcements and updates';
+ String get aboutTelegramChannelSubtitle => '공지 및 업데이트 안내';
@override
String get aboutTelegramChat => 'Telegram Community';
@override
- String get aboutTelegramChatSubtitle => 'Chat with other users';
+ String get aboutTelegramChatSubtitle => '다른 이용자와 소통';
@override
- String get aboutSocial => 'Social';
+ String get aboutSocial => '소셜';
@override
String get aboutApp => 'App';
@@ -329,244 +319,239 @@ class AppLocalizationsKo extends AppLocalizations {
@override
String get aboutBinimumDesc =>
- 'The creator of QQDL & HiFi API. Without this API, Tidal downloads wouldn\'t exist!';
+ 'QQDL 및 HiFi API 개발자입니다. 이 API가 없었다면 Tidal 다운로드는 불가능했을 것입니다!';
@override
- String get aboutSachinsenalDesc =>
- 'The original HiFi project creator. The foundation of Tidal integration!';
+ String get aboutSachinsenalDesc => '최초의 하이파이 프로젝트 창시자. 타이달 연동의 기반을 마련한 사람!';
@override
String get aboutSjdonadoDesc =>
- 'Creator of I Don\'t Have Spotify (IDHS). The fallback link resolver that saves the day!';
+ 'I Don\'t Have Spotify(IDHS) 개발자입니다. 위급 상황 발생 시 해결해 주는 대체 링크 해결 도구를 만들었습니다!';
@override
String get aboutDabMusic => 'DAB Music';
@override
String get aboutDabMusicDesc =>
- 'The best Qobuz streaming API. Hi-Res downloads wouldn\'t be possible without this!';
+ '최고의 Qobuz 스트리밍 API입니다. 이 API가 없었다면 고해상도 다운로드는 불가능했을 겁니다!';
@override
String get aboutSpotiSaver => 'SpotiSaver';
@override
String get aboutSpotiSaverDesc =>
- 'Tidal Hi-Res FLAC streaming endpoints. A key piece of the lossless puzzle!';
+ 'Tidal Hi-Res FLAC 스트리밍 엔드포인트. 무손실 음원 재생의 핵심 요소!';
@override
String get aboutAppDescription =>
- 'Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.';
+ 'Tidal, Qobuz, Amazon Music에서 Spotify 트랙을 무손실 음질로 다운로드하세요.';
@override
- String get artistAlbums => 'Albums';
+ String get artistAlbums => '앨범';
@override
- String get artistSingles => 'Singles & EPs';
+ String get artistSingles => '싱글 및 EP';
@override
- String get artistCompilations => 'Compilations';
+ String get artistCompilations => '편집';
@override
- String get artistPopular => 'Popular';
+ String get artistPopular => '인기순';
@override
String artistMonthlyListeners(String count) {
- return '$count monthly listeners';
+ return '월간 청취자: $count';
}
@override
- String get trackMetadataService => 'Service';
+ String get trackMetadataService => '제공업체';
@override
- String get trackMetadataPlay => 'Play';
+ String get trackMetadataPlay => '재생';
@override
- String get trackMetadataShare => 'Share';
+ String get trackMetadataShare => '공유';
@override
- String get trackMetadataDelete => 'Delete';
+ String get trackMetadataDelete => '삭제';
@override
- String get setupGrantPermission => 'Grant Permission';
+ String get setupGrantPermission => '권한을 제공해 주세요.';
@override
- String get setupSkip => 'Skip for now';
+ String get setupSkip => '다음에 할래요';
@override
- String get setupStorageAccessRequired => 'Storage Access Required';
+ String get setupStorageAccessRequired => '스토리지 접근 권한 필요';
@override
String get setupStorageAccessMessageAndroid11 =>
- 'Android 11+ requires \"All files access\" permission to save files to your chosen download folder.';
+ 'Android 11 이상 버전에서는 선택한 다운로드 폴더에 파일을 저장하려면 \"모든 파일 접근\" 권한이 필요합니다.';
@override
- String get setupOpenSettings => 'Open Settings';
+ String get setupOpenSettings => '설정으로 이동';
@override
String get setupPermissionDeniedMessage =>
- 'Permission denied. Please grant all permissions to continue.';
+ '권한이 거부되었습니다. 계속하려면 모든 권한을 허용해 주세요.';
@override
String setupPermissionRequired(String permissionType) {
- return '$permissionType Permission Required';
+ return '$permissionType 권한 필요';
}
@override
String setupPermissionRequiredMessage(String permissionType) {
- return '$permissionType permission is required for the best experience. You can change this later in Settings.';
+ return '최상의 사용 경험을 위해 $permissionType 권한이 필요합니다. 설정에서 나중에 변경할 수 있습니다.';
}
@override
- String get setupUseDefaultFolder => 'Use Default Folder?';
+ String get setupUseDefaultFolder => '기본 폴더를 사용하시겠습니까?';
@override
- String get setupNoFolderSelected =>
- 'No folder selected. Would you like to use the default Music folder?';
+ String get setupNoFolderSelected => '선택된 폴더가 없습니다. 기본 음악 폴더를 사용하시겠습니까?';
@override
- String get setupUseDefault => 'Use Default';
+ String get setupUseDefault => '기본값 사용';
@override
- String get setupDownloadLocationTitle => 'Download Location';
+ String get setupDownloadLocationTitle => '다운로드 경로';
@override
String get setupDownloadLocationIosMessage =>
- 'On iOS, downloads are saved to the app\'s Documents folder. You can access them via the Files app.';
+ 'iOS에서는 다운로드한 파일이 앱의 문서 폴더에 저장됩니다. 파일 앱을 통해 해당 파일에 접근할 수 있습니다.';
@override
- String get setupAppDocumentsFolder => 'App Documents Folder';
+ String get setupAppDocumentsFolder => '앱 문서 폴더';
@override
- String get setupAppDocumentsFolderSubtitle =>
- 'Recommended - accessible via Files app';
+ String get setupAppDocumentsFolderSubtitle => '권장 사항 - 파일 앱을 통해 접근 가능';
@override
- String get setupChooseFromFiles => 'Choose from Files';
+ String get setupChooseFromFiles => '파일 탐색기에서 선택';
@override
- String get setupChooseFromFilesSubtitle => 'Select iCloud or other location';
+ String get setupChooseFromFilesSubtitle => 'iCloud 또는 다른 위치를 선택하세요';
@override
String get setupIosEmptyFolderWarning =>
- 'iOS limitation: Empty folders cannot be selected. Choose a folder with at least one file.';
+ 'iOS 제한 사항: 빈 폴더는 선택할 수 없습니다. 파일이 하나 이상 있는 폴더를 선택하세요.';
@override
String get setupIcloudNotSupported =>
- 'iCloud Drive is not supported. Please use the app Documents folder.';
+ 'iCloud Drive는 지원되지 않습니다. 앱의 문서 폴더를 사용해 주세요.';
@override
- String get setupDownloadInFlac => 'Download Spotify tracks in FLAC';
+ String get setupDownloadInFlac => 'Spotify 음악을 FLAC 형식으로 다운로드하세요.';
@override
- String get setupStorageGranted => 'Storage Permission Granted!';
+ String get setupStorageGranted => '저장소 접근 권한이 부여되었습니다!';
@override
- String get setupStorageRequired => 'Storage Permission Required';
+ String get setupStorageRequired => '저장소 접근 권한이 필요합니다.';
@override
String get setupStorageDescription =>
- 'SpotiFLAC needs storage permission to save your downloaded music files.';
+ 'SpotiFLAC은 다운로드한 음악 파일을 저장하기 위해 저장소 접근 권한이 필요합니다.';
@override
- String get setupNotificationGranted => 'Notification Permission Granted!';
+ String get setupNotificationGranted => '알림 권한이 부여되었습니다!';
@override
- String get setupNotificationEnable => 'Enable Notifications';
+ String get setupNotificationEnable => '알림 활성화';
@override
- String get setupFolderChoose => 'Choose Download Folder';
+ String get setupFolderChoose => '다운로드 폴더를 선택하세요';
@override
- String get setupFolderDescription =>
- 'Select a folder where your downloaded music will be saved.';
+ String get setupFolderDescription => '다운로드한 음악 파일이 저장될 폴더를 선택하세요.';
@override
- String get setupSelectFolder => 'Select Folder';
+ String get setupSelectFolder => '폴더 선택';
@override
- String get setupEnableNotifications => 'Enable Notifications';
+ String get setupEnableNotifications => '알림 활성화';
@override
String get setupNotificationBackgroundDescription =>
- 'Get notified about download progress and completion. This helps you track downloads when the app is in background.';
+ '알림으로 다운로드 진행 상황을 확인하세요. 앱이 백그라운드에서 실행 중일 때 다운로드 상태와 완료 여부를 확인할 수 있습니다.';
@override
- String get setupSkipForNow => 'Skip for now';
+ String get setupSkipForNow => '다음에 할래요.';
@override
- String get setupNext => 'Next';
+ String get setupNext => '다음';
@override
- String get setupGetStarted => 'Get Started';
+ String get setupGetStarted => '시작하기';
@override
String get setupAllowAccessToManageFiles =>
- 'Please enable \"Allow access to manage all files\" in the next screen.';
+ '다음 화면에서 \"모든 파일 관리 권한 허용\"을 활성화해 주세요.';
@override
- String get dialogCancel => 'Cancel';
+ String get dialogCancel => '취소';
@override
- String get dialogSave => 'Save';
+ String get dialogSave => '저장';
@override
- String get dialogDelete => 'Delete';
+ String get dialogDelete => '삭제';
@override
- String get dialogRetry => 'Retry';
+ String get dialogRetry => '재시도';
@override
- String get dialogClear => 'Clear';
+ String get dialogClear => '지우기';
@override
- String get dialogDone => 'Done';
+ String get dialogDone => '완료';
@override
- String get dialogImport => 'Import';
+ String get dialogImport => '불러오기';
@override
- String get dialogDiscard => 'Discard';
+ String get dialogDiscard => '취소';
@override
- String get dialogRemove => 'Remove';
+ String get dialogRemove => '제거';
@override
- String get dialogUninstall => 'Uninstall';
+ String get dialogUninstall => '삭제';
@override
- String get dialogDiscardChanges => 'Discard Changes?';
+ String get dialogDiscardChanges => '변경사항 취소';
@override
- String get dialogUnsavedChanges =>
- 'You have unsaved changes. Do you want to discard them?';
+ String get dialogUnsavedChanges => '저장되지 않은 변경 사항이 있습니다. 삭제하시겠습니까?';
@override
- String get dialogClearAll => 'Clear All';
+ String get dialogClearAll => '모두 제거:';
@override
- String get dialogRemoveExtension => 'Remove Extension';
+ String get dialogRemoveExtension => '확장 프로그램 제거';
@override
String get dialogRemoveExtensionMessage =>
- 'Are you sure you want to remove this extension? This cannot be undone.';
+ '이 확장 프로그램을 정말로 제거하시겠습니까? 이 작업은 되돌릴 수 없습니다.';
@override
- String get dialogUninstallExtension => 'Uninstall Extension?';
+ String get dialogUninstallExtension => '확장 프로그램을 제거하시겠습니까?';
@override
String dialogUninstallExtensionMessage(String extensionName) {
- return 'Are you sure you want to remove $extensionName?';
+ return '$extensionName을 정말로 삭제하시겠습니까?';
}
@override
- String get dialogClearHistoryTitle => 'Clear History';
+ String get dialogClearHistoryTitle => '기록 삭제';
@override
String get dialogClearHistoryMessage =>
- 'Are you sure you want to clear all download history? This cannot be undone.';
+ '다운로드 기록을 모두 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.';
@override
- String get dialogDeleteSelectedTitle => 'Delete Selected';
+ String get dialogDeleteSelectedTitle => '선택한 항목 삭제';
@override
String dialogDeleteSelectedMessage(int count) {
@@ -576,50 +561,50 @@ class AppLocalizationsKo extends AppLocalizations {
other: 'tracks',
one: 'track',
);
- return 'Delete $count $_temp0 from history?\n\nThis will also delete the files from storage.';
+ return '기록에서 $count $_temp0를 삭제하시겠습니까?';
}
@override
- String get dialogImportPlaylistTitle => 'Import Playlist';
+ String get dialogImportPlaylistTitle => '재생 목록 가져오기';
@override
String dialogImportPlaylistMessage(int count) {
- return 'Found $count tracks in CSV. Add them to download queue?';
+ return 'CSV 파일에서 $count개의 트랙을 찾았습니다. 다운로드 대기열에 추가하시겠습니까?';
}
@override
String csvImportTracks(int count) {
- return '$count tracks from CSV';
+ return 'CSV 파일의 트랙: $count';
}
@override
String snackbarAddedToQueue(String trackName) {
- return 'Added \"$trackName\" to queue';
+ return '\"$trackName\"(을)를 대기열에 추가했습니다.';
}
@override
String snackbarAddedTracksToQueue(int count) {
- return 'Added $count tracks to queue';
+ return '대기열에 $count개의 트랙을 추가했습니다.';
}
@override
String snackbarAlreadyDownloaded(String trackName) {
- return '\"$trackName\" already downloaded';
+ return '\"$trackName\"(은)는 이미 다운로드되었습니다.';
}
@override
String snackbarAlreadyInLibrary(String trackName) {
- return '\"$trackName\" already exists in your library';
+ return '라이브러리에 \"$trackName\"(은)는 이미 존재합니다.';
}
@override
- String get snackbarHistoryCleared => 'History cleared';
+ String get snackbarHistoryCleared => '기록 삭제됨';
@override
- String get snackbarCredentialsSaved => 'Credentials saved';
+ String get snackbarCredentialsSaved => '자격 증명이 저장되었습니다.';
@override
- String get snackbarCredentialsCleared => 'Credentials cleared';
+ String get snackbarCredentialsCleared => '자격 증명이 제거되었습니다.';
@override
String snackbarDeletedTracks(int count) {
@@ -629,136 +614,152 @@ class AppLocalizationsKo extends AppLocalizations {
other: 'tracks',
one: 'track',
);
- return 'Deleted $count $_temp0';
+ return '$count$_temp0 제거됨';
}
@override
String snackbarCannotOpenFile(String error) {
- return 'Cannot open file: $error';
+ return '파일을 열 수 없습니다: $error';
}
@override
- String get snackbarFillAllFields => 'Please fill all fields';
+ String get snackbarFillAllFields => '모든 항목을 입력해 주세요.';
@override
String get snackbarViewQueue => 'View Queue';
@override
String snackbarUrlCopied(String platform) {
- return '$platform URL copied to clipboard';
+ return '$platform 링크가 클립보드에 저장됨';
}
@override
- String get snackbarFileNotFound => 'File not found';
+ String get snackbarFileNotFound => '파일을 찾을 수 없음';
@override
- String get snackbarSelectExtFile => 'Please select a .spotiflac-ext file';
+ String get snackbarSelectExtFile => '.spotiflac-ext 확장자 파일을 선택';
@override
- String get snackbarProviderPrioritySaved => 'Provider priority saved';
+ String get snackbarProviderPrioritySaved => '제공자 우선순위 저장됨';
@override
- String get snackbarMetadataProviderSaved =>
- 'Metadata provider priority saved';
+ String get snackbarMetadataProviderSaved => '메타데이터 제공자 우선순위 저장됨';
@override
String snackbarExtensionInstalled(String extensionName) {
- return '$extensionName installed.';
+ return '$extensionName(이)가 설치됨';
}
@override
String snackbarExtensionUpdated(String extensionName) {
- return '$extensionName updated.';
+ return '$extensionName(이)가 설치됨.';
}
@override
- String get snackbarFailedToInstall => 'Failed to install extension';
+ String get snackbarFailedToInstall => '확장 프로그램 설치 실패';
@override
- String get snackbarFailedToUpdate => 'Failed to update extension';
+ String get snackbarFailedToUpdate => '확장 프로그램 업데이트 실패';
@override
String get errorRateLimited => 'Rate Limited';
@override
- String get errorRateLimitedMessage =>
- 'Too many requests. Please wait a moment before searching again.';
+ String get errorRateLimitedMessage => '요청이 너무 많습니다. 잠시 후 다시 검색해 주세요.';
@override
- String get errorNoTracksFound => 'No tracks found';
+ String get errorNoTracksFound => '트랙을 찾을 수 없습니다';
+
+ @override
+ String get errorUrlNotRecognized => 'Link not recognized';
+
+ @override
+ String get errorUrlNotRecognizedMessage =>
+ 'This link is not supported. Make sure the URL is correct and a compatible extension is installed.';
+
+ @override
+ String get errorUrlFetchFailed =>
+ 'Failed to load content from this link. Please try again.';
@override
String errorMissingExtensionSource(String item) {
- return 'Cannot load $item: missing extension source';
+ return '확장 소스가 누락되어, $item(을)를 로드할 수 없습니다';
}
@override
- String get actionPause => 'Pause';
+ String get actionPause => '멈추기';
@override
- String get actionResume => 'Resume';
+ String get actionResume => '재시작';
@override
- String get actionCancel => 'Cancel';
+ String get actionCancel => '취소';
@override
- String get actionSelectAll => 'Select All';
+ String get actionSelectAll => '모두 선택';
@override
- String get actionDeselect => 'Deselect';
+ String get actionDeselect => '선택 해제';
@override
- String get actionRemoveCredentials => 'Remove Credentials';
+ String get actionRemoveCredentials => '자격 증명 제거';
@override
- String get actionSaveCredentials => 'Save Credentials';
+ String get actionSaveCredentials => '자격 증명 저장';
@override
String selectionSelected(int count) {
- return '$count selected';
+ return '$count개 선택됨';
}
@override
- String get selectionAllSelected => 'All tracks selected';
+ String get selectionAllSelected => '모든 트랙 선택됨';
@override
- String get selectionSelectToDelete => 'Select tracks to delete';
+ String get selectionSelectToDelete => '삭제할 트랙을 선택';
@override
String progressFetchingMetadata(int current, int total) {
- return 'Fetching metadata... $current/$total';
+ return '메타데이터 가져오는 중... $current/$total';
}
@override
- String get progressReadingCsv => 'Reading CSV...';
+ String get progressReadingCsv => 'CSV 파일을 읽는 중...';
@override
- String get searchSongs => 'Songs';
+ String get searchSongs => '곡들';
@override
- String get searchArtists => 'Artists';
+ String get searchArtists => '아티스트들';
@override
- String get searchAlbums => 'Albums';
+ String get searchAlbums => '앨범들';
@override
- String get searchPlaylists => 'Playlists';
+ String get searchPlaylists => '재생목록들';
@override
- String get tooltipPlay => 'Play';
+ String get tooltipPlay => '재생';
@override
- String get filenameFormat => 'Filename Format';
+ String get filenameFormat => '';
@override
- String get filenameShowAdvancedTags => 'Show advanced tags';
+ String get filenameShowAdvancedTags => '고급 태그 표시';
@override
String get filenameShowAdvancedTagsDescription =>
- 'Enable formatted tags for track padding and date patterns';
+ '트랙 패딩 및 날짜 패턴에 대한 서식 있는 태그를 활성화합니다.';
@override
- String get folderOrganizationNone => 'No organization';
+ String get folderOrganizationNone => '정리하지 않음';
+
+ @override
+ String get folderOrganizationByPlaylist => 'By Playlist';
+
+ @override
+ String get folderOrganizationByPlaylistSubtitle =>
+ 'Separate folder for each playlist';
@override
String get folderOrganizationByArtist => 'By Artist';
@@ -877,116 +878,114 @@ class AppLocalizationsKo extends AppLocalizations {
String get logShareLogs => 'Share logs';
@override
- String get logClearLogs => 'Clear logs';
+ String get logClearLogs => '로그 제거';
@override
- String get logClearLogsTitle => 'Clear Logs';
+ String get logClearLogsTitle => '로그 제거';
@override
- String get logClearLogsMessage => 'Are you sure you want to clear all logs?';
+ String get logClearLogsMessage => '모든 로그를 삭제하시겠습니까?';
@override
- String get logFilterBySeverity => 'Filter logs by severity';
+ String get logFilterBySeverity => '심각성에 따라 로그 분류';
@override
- String get logNoLogsYet => 'No logs yet';
+ String get logNoLogsYet => '어떠한 로그도 없음';
@override
- String get logNoLogsYetSubtitle => 'Logs will appear here as you use the app';
+ String get logNoLogsYetSubtitle => '앱을 사용하는 동안 로그가 여기에 표시됩니다.';
@override
String logEntriesFiltered(int count) {
- return 'Entries ($count filtered)';
+ return '($count filtered)개 항목 필터링';
}
@override
String logEntries(int count) {
- return 'Entries ($count)';
+ return '항목 수: ($count)';
}
@override
- String get credentialsTitle => 'Spotify Credentials';
+ String get credentialsTitle => 'Spotify 자격 증명';
@override
String get credentialsDescription =>
- 'Enter your Client ID and Secret to use your own Spotify application quota.';
+ 'Spotify 애플리케이션 할당량을 사용하려면 클라이언트 ID와 비밀키를 입력하세요.';
@override
String get credentialsClientId => 'Client ID';
@override
- String get credentialsClientIdHint => 'Paste Client ID';
+ String get credentialsClientIdHint => 'Client ID를 붙여넣으세요';
@override
- String get credentialsClientSecret => 'Client Secret';
+ String get credentialsClientSecret => '비밀키';
@override
- String get credentialsClientSecretHint => 'Paste Client Secret';
+ String get credentialsClientSecretHint => '비밀키를 붙여넣으세요';
@override
- String get channelStable => 'Stable';
+ String get channelStable => '안정';
@override
- String get channelPreview => 'Preview';
+ String get channelPreview => '베타';
@override
- String get sectionSearchSource => 'Search Source';
+ String get sectionSearchSource => '검색 소스';
@override
- String get sectionDownload => 'Download';
+ String get sectionDownload => '다운로드';
@override
- String get sectionPerformance => 'Performance';
+ String get sectionPerformance => '성능';
@override
- String get sectionApp => 'App';
+ String get sectionApp => '앱';
@override
- String get sectionData => 'Data';
+ String get sectionData => '데이터';
@override
String get sectionDebug => 'Debug';
@override
- String get sectionService => 'Service';
+ String get sectionService => '서비스';
@override
- String get sectionAudioQuality => 'Audio Quality';
+ String get sectionAudioQuality => '오디오 품질';
@override
- String get sectionFileSettings => 'File Settings';
+ String get sectionFileSettings => '파일 설정';
@override
- String get sectionLyrics => 'Lyrics';
+ String get sectionLyrics => '가사';
@override
- String get lyricsMode => 'Lyrics Mode';
+ String get lyricsMode => '가사 설정';
@override
- String get lyricsModeDescription =>
- 'Choose how lyrics are saved with your downloads';
+ String get lyricsModeDescription => '다운로드한 파일에 가사를 저장하는 방법을 선택하세요.';
@override
- String get lyricsModeEmbed => 'Embed in file';
+ String get lyricsModeEmbed => '파일에 포함';
@override
- String get lyricsModeEmbedSubtitle => 'Lyrics stored inside FLAC metadata';
+ String get lyricsModeEmbedSubtitle => 'FLAC 메타데이터 내에 저장됩니다.';
@override
- String get lyricsModeExternal => 'External .lrc file';
+ String get lyricsModeExternal => '외부 .lrc 파일';
@override
- String get lyricsModeExternalSubtitle =>
- 'Separate .lrc file for players like Samsung Music';
+ String get lyricsModeExternalSubtitle => '삼성 뮤직과 같은 플레이어용 별도 .lrc 파일';
@override
- String get lyricsModeBoth => 'Both';
+ String get lyricsModeBoth => '둘 다';
@override
- String get lyricsModeBothSubtitle => 'Embed and save .lrc file';
+ String get lyricsModeBothSubtitle => '.lrc 파일을 삽입하고 저장합니다.';
@override
- String get sectionColor => 'Color';
+ String get sectionColor => '색상';
@override
String get sectionTheme => 'Theme';
@@ -2125,6 +2124,54 @@ class AppLocalizationsKo extends AppLocalizations {
@override
String get trackConvertFailed => 'Conversion failed';
+ @override
+ String get cueSplitTitle => 'Split CUE Sheet';
+
+ @override
+ String get cueSplitSubtitle => 'Split CUE+FLAC into individual tracks';
+
+ @override
+ String cueSplitAlbum(String album) {
+ return 'Album: $album';
+ }
+
+ @override
+ String cueSplitArtist(String artist) {
+ return 'Artist: $artist';
+ }
+
+ @override
+ String cueSplitTrackCount(int count) {
+ return '$count tracks';
+ }
+
+ @override
+ String get cueSplitConfirmTitle => 'Split CUE Album';
+
+ @override
+ String cueSplitConfirmMessage(String album, int count) {
+ return 'Split \"$album\" into $count individual FLAC files?\n\nFiles will be saved to the same directory.';
+ }
+
+ @override
+ String cueSplitSplitting(int current, int total) {
+ return 'Splitting CUE sheet... ($current/$total)';
+ }
+
+ @override
+ String cueSplitSuccess(int count) {
+ return 'Split into $count tracks successfully';
+ }
+
+ @override
+ String get cueSplitFailed => 'CUE split failed';
+
+ @override
+ String get cueSplitNoAudioFile => 'Audio file not found for this CUE sheet';
+
+ @override
+ String get cueSplitButton => 'Split into Tracks';
+
@override
String get actionCreate => 'Create';
diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart
index be1be644..5f4975d9 100644
--- a/lib/l10n/app_localizations_nl.dart
+++ b/lib/l10n/app_localizations_nl.dart
@@ -688,6 +688,17 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get errorNoTracksFound => 'No tracks found';
+ @override
+ String get errorUrlNotRecognized => 'Link not recognized';
+
+ @override
+ String get errorUrlNotRecognizedMessage =>
+ 'This link is not supported. Make sure the URL is correct and a compatible extension is installed.';
+
+ @override
+ String get errorUrlFetchFailed =>
+ 'Failed to load content from this link. Please try again.';
+
@override
String errorMissingExtensionSource(String item) {
return 'Cannot load $item: missing extension source';
@@ -761,6 +772,13 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get folderOrganizationNone => 'No organization';
+ @override
+ String get folderOrganizationByPlaylist => 'By Playlist';
+
+ @override
+ String get folderOrganizationByPlaylistSubtitle =>
+ 'Separate folder for each playlist';
+
@override
String get folderOrganizationByArtist => 'By Artist';
@@ -2126,6 +2144,54 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get trackConvertFailed => 'Conversion failed';
+ @override
+ String get cueSplitTitle => 'Split CUE Sheet';
+
+ @override
+ String get cueSplitSubtitle => 'Split CUE+FLAC into individual tracks';
+
+ @override
+ String cueSplitAlbum(String album) {
+ return 'Album: $album';
+ }
+
+ @override
+ String cueSplitArtist(String artist) {
+ return 'Artist: $artist';
+ }
+
+ @override
+ String cueSplitTrackCount(int count) {
+ return '$count tracks';
+ }
+
+ @override
+ String get cueSplitConfirmTitle => 'Split CUE Album';
+
+ @override
+ String cueSplitConfirmMessage(String album, int count) {
+ return 'Split \"$album\" into $count individual FLAC files?\n\nFiles will be saved to the same directory.';
+ }
+
+ @override
+ String cueSplitSplitting(int current, int total) {
+ return 'Splitting CUE sheet... ($current/$total)';
+ }
+
+ @override
+ String cueSplitSuccess(int count) {
+ return 'Split into $count tracks successfully';
+ }
+
+ @override
+ String get cueSplitFailed => 'CUE split failed';
+
+ @override
+ String get cueSplitNoAudioFile => 'Audio file not found for this CUE sheet';
+
+ @override
+ String get cueSplitButton => 'Split into Tracks';
+
@override
String get actionCreate => 'Create';
diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart
index 499adcbb..62b648ae 100644
--- a/lib/l10n/app_localizations_pt.dart
+++ b/lib/l10n/app_localizations_pt.dart
@@ -356,7 +356,7 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get aboutAppDescription =>
- 'Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.';
+ 'Download Spotify tracks in lossless quality from Tidal and Qobuz.';
@override
String get artistAlbums => 'Albums';
@@ -688,6 +688,17 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get errorNoTracksFound => 'No tracks found';
+ @override
+ String get errorUrlNotRecognized => 'Link not recognized';
+
+ @override
+ String get errorUrlNotRecognizedMessage =>
+ 'This link is not supported. Make sure the URL is correct and a compatible extension is installed.';
+
+ @override
+ String get errorUrlFetchFailed =>
+ 'Failed to load content from this link. Please try again.';
+
@override
String errorMissingExtensionSource(String item) {
return 'Cannot load $item: missing extension source';
@@ -761,6 +772,13 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get folderOrganizationNone => 'No organization';
+ @override
+ String get folderOrganizationByPlaylist => 'By Playlist';
+
+ @override
+ String get folderOrganizationByPlaylistSubtitle =>
+ 'Separate folder for each playlist';
+
@override
String get folderOrganizationByArtist => 'By Artist';
@@ -1809,7 +1827,7 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get tutorialWelcomeTip2 =>
- 'Get FLAC quality audio from Tidal, Qobuz, or Amazon Music';
+ 'Get FLAC quality audio from Tidal, Qobuz, or Deezer';
@override
String get tutorialWelcomeTip3 =>
@@ -2126,6 +2144,54 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get trackConvertFailed => 'Conversion failed';
+ @override
+ String get cueSplitTitle => 'Split CUE Sheet';
+
+ @override
+ String get cueSplitSubtitle => 'Split CUE+FLAC into individual tracks';
+
+ @override
+ String cueSplitAlbum(String album) {
+ return 'Album: $album';
+ }
+
+ @override
+ String cueSplitArtist(String artist) {
+ return 'Artist: $artist';
+ }
+
+ @override
+ String cueSplitTrackCount(int count) {
+ return '$count tracks';
+ }
+
+ @override
+ String get cueSplitConfirmTitle => 'Split CUE Album';
+
+ @override
+ String cueSplitConfirmMessage(String album, int count) {
+ return 'Split \"$album\" into $count individual FLAC files?\n\nFiles will be saved to the same directory.';
+ }
+
+ @override
+ String cueSplitSplitting(int current, int total) {
+ return 'Splitting CUE sheet... ($current/$total)';
+ }
+
+ @override
+ String cueSplitSuccess(int count) {
+ return 'Split into $count tracks successfully';
+ }
+
+ @override
+ String get cueSplitFailed => 'CUE split failed';
+
+ @override
+ String get cueSplitNoAudioFile => 'Audio file not found for this CUE sheet';
+
+ @override
+ String get cueSplitButton => 'Split into Tracks';
+
@override
String get actionCreate => 'Create';
@@ -2705,7 +2771,7 @@ class AppLocalizationsPtPt extends AppLocalizationsPt {
@override
String get aboutAppDescription =>
- 'Baixe faixas do Spotify em qualidade sem perdas do Tidal, Qobuz e Amazon Music.';
+ 'Baixe faixas do Spotify em qualidade sem perdas do Tidal e Qobuz.';
@override
String get artistAlbums => 'Álbuns';
@@ -4147,7 +4213,7 @@ class AppLocalizationsPtPt extends AppLocalizationsPt {
@override
String get tutorialWelcomeTip2 =>
- 'Get FLAC quality audio from Tidal, Qobuz, or Amazon Music';
+ 'Obtenha áudio em qualidade FLAC do Tidal, Qobuz ou Deezer';
@override
String get tutorialWelcomeTip3 =>
diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart
index 85af897d..66c75d45 100644
--- a/lib/l10n/app_localizations_ru.dart
+++ b/lib/l10n/app_localizations_ru.dart
@@ -67,7 +67,7 @@ class AppLocalizationsRu extends AppLocalizations {
String get settingsAbout => 'О программе';
@override
- String get downloadTitle => 'Скачивание';
+ String get downloadTitle => 'Скачать';
@override
String get downloadAskQualitySubtitle =>
@@ -146,11 +146,11 @@ class AppLocalizationsRu extends AppLocalizations {
'Использование только встроенных провайдеров';
@override
- String get optionsEmbedLyrics => 'Вставить текст песни';
+ String get optionsEmbedLyrics => 'Вписать текст песни';
@override
String get optionsEmbedLyricsSubtitle =>
- 'Вставить синхронизированные тексты в FLAC файлы';
+ 'Вписать синхронизированные тексты во FLAC файлы';
@override
String get optionsMaxQualityCover => 'Максимальное качество обложки';
@@ -337,7 +337,7 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get aboutBinimumDesc =>
- 'Создатель QQDL & HiFi API. Без этого API загрузки Tidal не существовали бы!';
+ 'Создатель QQDL & HiFi API. Без него API загрузки Tidal не существовали бы!';
@override
String get aboutSachinsenalDesc =>
@@ -601,7 +601,7 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String csvImportTracks(int count) {
- return '$count треков из CSV';
+ return '$count трек(-ов) из CSV';
}
@override
@@ -702,6 +702,17 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get errorNoTracksFound => 'Треки не найдены';
+ @override
+ String get errorUrlNotRecognized => 'Link not recognized';
+
+ @override
+ String get errorUrlNotRecognizedMessage =>
+ 'This link is not supported. Make sure the URL is correct and a compatible extension is installed.';
+
+ @override
+ String get errorUrlFetchFailed =>
+ 'Failed to load content from this link. Please try again.';
+
@override
String errorMissingExtensionSource(String item) {
return 'Невозможно загрузить $item: отсутствует источник расширения';
@@ -766,15 +777,22 @@ class AppLocalizationsRu extends AppLocalizations {
String get filenameFormat => 'Формат имени файла';
@override
- String get filenameShowAdvancedTags => 'Show advanced tags';
+ String get filenameShowAdvancedTags => 'Показать расширенные теги';
@override
String get filenameShowAdvancedTagsDescription =>
- 'Enable formatted tags for track padding and date patterns';
+ 'Включить форматированные теги для отслеживания заполнения и шаблонов дат';
@override
String get folderOrganizationNone => 'Без организации';
+ @override
+ String get folderOrganizationByPlaylist => 'By Playlist';
+
+ @override
+ String get folderOrganizationByPlaylistSubtitle =>
+ 'Separate folder for each playlist';
+
@override
String get folderOrganizationByArtist => 'По исполнителю';
@@ -983,7 +1001,7 @@ class AppLocalizationsRu extends AppLocalizations {
'Выберите как сохранить тексты песен при скачивании';
@override
- String get lyricsModeEmbed => 'Вставить в файл';
+ String get lyricsModeEmbed => 'Вписать в файл';
@override
String get lyricsModeEmbedSubtitle => 'Встроить текст в метаданные FLAC';
@@ -999,7 +1017,7 @@ class AppLocalizationsRu extends AppLocalizations {
String get lyricsModeBoth => 'Оба варианта';
@override
- String get lyricsModeBothSubtitle => 'Вставить и сохранить файл .lrc';
+ String get lyricsModeBothSubtitle => 'Вписать и сохранить .lrc файл';
@override
String get sectionColor => 'Цвет';
@@ -1138,7 +1156,7 @@ class AppLocalizationsRu extends AppLocalizations {
String get trackLyricsLoadFailed => 'Не удалось загрузить текст песни';
@override
- String get trackEmbedLyrics => 'Вставить текст песни';
+ String get trackEmbedLyrics => 'Вписать текст песни';
@override
String get trackLyricsEmbedded => 'Текст успешно добавлен';
@@ -1361,10 +1379,10 @@ class AppLocalizationsRu extends AppLocalizations {
'YouTube обеспечивает только звук с потерями(Lossy).';
@override
- String get youtubeOpusBitrateTitle => 'YouTube Opus Bitrate';
+ String get youtubeOpusBitrateTitle => 'Битрейт YouTube Opus';
@override
- String get youtubeMp3BitrateTitle => 'YouTube MP3 Bitrate';
+ String get youtubeMp3BitrateTitle => 'Битрейт YouTube MP3';
@override
String get downloadAskBeforeDownload => 'Спрашивать перед скачиванием';
@@ -1383,7 +1401,8 @@ class AppLocalizationsRu extends AppLocalizations {
'Использовать исполнителя альбома для папок';
@override
- String get downloadUsePrimaryArtistOnly => 'Primary artist only for folders';
+ String get downloadUsePrimaryArtistOnly =>
+ 'Основной исполнитель только для папок';
@override
String get downloadUsePrimaryArtistOnlyEnabled =>
@@ -1391,7 +1410,7 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get downloadUsePrimaryArtistOnlyDisabled =>
- 'Full artist string used for folder name';
+ 'Полная строка исполнителя, используемая для имени папки';
@override
String get downloadSelectQuality => 'Выбор качества';
@@ -1423,7 +1442,7 @@ class AppLocalizationsRu extends AppLocalizations {
String get settingsDownloadNetwork => 'Сеть для скачивания';
@override
- String get settingsDownloadNetworkAny => 'WiFi и мобильная сеть';
+ String get settingsDownloadNetworkAny => 'WiFi и Мобильная сеть';
@override
String get settingsDownloadNetworkWifiOnly => 'Только WiFi';
@@ -1712,8 +1731,10 @@ class AppLocalizationsRu extends AppLocalizations {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
- other: 'tracks',
- one: 'track',
+ other: 'треков',
+ many: 'треков',
+ few: 'трека',
+ one: 'трек',
);
return '$_temp0';
}
@@ -1800,7 +1821,7 @@ class AppLocalizationsRu extends AppLocalizations {
String get libraryFilterQualityCD => 'CD (16 бит)';
@override
- String get libraryFilterQualityLossy => 'С потерями';
+ String get libraryFilterQualityLossy => 'Lossy';
@override
String get libraryFilterFormat => 'Формат';
@@ -1904,7 +1925,7 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get tutorialExtensionsTip1 =>
- 'Browse the Store tab to discover useful extensions';
+ 'Просмотрите вкладку Магазина, чтобы найти полезные расширения';
@override
String get tutorialExtensionsTip2 =>
@@ -1912,14 +1933,14 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get tutorialExtensionsTip3 =>
- 'Get lyrics, enhanced metadata, and more features';
+ 'Получайте тексты песен, улучшенные метаданные и другие возможности';
@override
String get tutorialSettingsTitle => 'Настройте приложение под себя';
@override
String get tutorialSettingsDesc =>
- 'Personalize the app in Settings to match your preferences.';
+ 'Персонализируйте приложение в Настройках, чтобы оно соответствовало вашим предпочтениям.';
@override
String get tutorialSettingsTip1 =>
@@ -1944,11 +1965,11 @@ class AppLocalizationsRu extends AppLocalizations {
'Пересканировать все файлы, игнорировать кэш';
@override
- String get cleanupOrphanedDownloads => 'Cleanup Orphaned Downloads';
+ String get cleanupOrphanedDownloads => 'Очистка отложенных скачиваний';
@override
String get cleanupOrphanedDownloadsSubtitle =>
- 'Remove history entries for files that no longer exist';
+ 'Удалить историю записи для файлов, которых больше не существует';
@override
String cleanupOrphanedDownloadsResult(int count) {
@@ -1956,7 +1977,7 @@ class AppLocalizationsRu extends AppLocalizations {
}
@override
- String get cleanupOrphanedDownloadsNone => 'No orphaned entries found';
+ String get cleanupOrphanedDownloadsNone => 'Записей без описания не найдено';
@override
String get cacheTitle => 'Хранилище и кэш';
@@ -1966,11 +1987,11 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get cacheSummarySubtitle =>
- 'Clearing cache will not remove downloaded music files.';
+ 'Очистка кэша не приведет к удалению загруженных музыкальных файлов.';
@override
String cacheEstimatedTotal(String size) {
- return 'Estimated cache usage: $size';
+ return 'Приблизительное использование кэша: $size';
}
@override
@@ -1984,42 +2005,42 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get cacheAppDirectoryDesc =>
- 'HTTP responses, WebView data, and other temporary app data.';
+ 'HTTP-ответы, данные WebView и другие временные данные приложения.';
@override
- String get cacheTempDirectory => 'Temporary directory';
+ String get cacheTempDirectory => 'Временная директория';
@override
String get cacheTempDirectoryDesc =>
- 'Temporary files from downloads and audio conversion.';
+ 'Временные файлы из загрузок и аудио конвертации.';
@override
- String get cacheCoverImage => 'Cover image cache';
+ String get cacheCoverImage => 'Кэш обложек';
@override
String get cacheCoverImageDesc =>
- 'Downloaded album and track cover art. Will re-download when viewed.';
+ 'Скачанный альбом и трек обложки. Будет заново скачан после просмотра.';
@override
- String get cacheLibraryCover => 'Library cover cache';
+ String get cacheLibraryCover => 'Кэш обложек библиотеки';
@override
String get cacheLibraryCoverDesc =>
- 'Cover art extracted from local music files. Will re-extract on next scan.';
+ 'Обложка извлечена из локальных музыкальных файлов. Будет повторно извлечено при следующем сканировании.';
@override
- String get cacheExploreFeed => 'Explore feed cache';
+ String get cacheExploreFeed => 'Просмотреть кэш ленты';
@override
String get cacheExploreFeedDesc =>
- 'Explore tab content (new releases, trending). Will refresh on next visit.';
+ 'Изучите содержимое вкладки (новые релизы, тренды). Они обновятся при следующем посещении.';
@override
- String get cacheTrackLookup => 'Track lookup cache';
+ String get cacheTrackLookup => 'Отслеживать кэш поиска';
@override
String get cacheTrackLookupDesc =>
- 'Spotify/Deezer track ID lookups. Clearing may slow next few searches.';
+ 'Поиск ID трека в Spotify/Deezer. Очистка может замедлить следующие несколько поисков.';
@override
String get cacheCleanupUnusedDesc =>
@@ -2040,7 +2061,7 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String cacheEntries(int count) {
- return '$count entries';
+ return '$count записей';
}
@override
@@ -2053,7 +2074,7 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String cacheClearConfirmMessage(String target) {
- return 'This will clear cached data for $target. Downloaded music files will not be deleted.';
+ return 'Это очистит кэш для $target. Загруженные музыкальные файлы не будут удалены.';
}
@override
@@ -2075,7 +2096,7 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String cacheCleanupResult(int downloadCount, int libraryCount) {
- return 'Cleanup completed: $downloadCount orphaned downloads, $libraryCount missing library entries';
+ return 'Очистка завершена: $downloadCount потерянных загрузок, $libraryCount отсутствующих записей в библиотеке';
}
@override
@@ -2095,14 +2116,14 @@ class AppLocalizationsRu extends AppLocalizations {
'Получить и сохранить текст песни в формате .lrc';
@override
- String get trackSaveLyricsProgress => 'Saving lyrics...';
+ String get trackSaveLyricsProgress => 'Сохранение текста...';
@override
- String get trackReEnrich => 'Re-enrich';
+ String get trackReEnrich => 'Обновить';
@override
String get trackReEnrichOnlineSubtitle =>
- 'Search metadata online and embed into file';
+ 'Поиск в сети метаданных и встраивание в файл';
@override
String get trackEditMetadata => 'Редактировать метаданные';
@@ -2121,13 +2142,13 @@ class AppLocalizationsRu extends AppLocalizations {
}
@override
- String get trackReEnrichProgress => 'Re-enriching metadata...';
+ String get trackReEnrichProgress => 'Обновление метаданных...';
@override
String get trackReEnrichSearching => 'Поиск метаданных в сети...';
@override
- String get trackReEnrichSuccess => 'Metadata re-enriched successfully';
+ String get trackReEnrichSuccess => 'Метаданные успешно обновлены';
@override
String get trackReEnrichFfmpegFailed =>
@@ -2139,22 +2160,22 @@ class AppLocalizationsRu extends AppLocalizations {
}
@override
- String get trackConvertFormat => 'Convert Format';
+ String get trackConvertFormat => 'Переконвертировать формат';
@override
- String get trackConvertFormatSubtitle => 'Convert to MP3 or Opus';
+ String get trackConvertFormatSubtitle => 'Конвертировать в MP3 или Opus';
@override
- String get trackConvertTitle => 'Convert Audio';
+ String get trackConvertTitle => 'Конвертировать аудио';
@override
- String get trackConvertTargetFormat => 'Target Format';
+ String get trackConvertTargetFormat => 'Целевой формат';
@override
- String get trackConvertBitrate => 'Bitrate';
+ String get trackConvertBitrate => 'Битрейт';
@override
- String get trackConvertConfirmTitle => 'Confirm Conversion';
+ String get trackConvertConfirmTitle => 'Подтвердить конвертацию';
@override
String trackConvertConfirmMessage(
@@ -2162,177 +2183,229 @@ class AppLocalizationsRu extends AppLocalizations {
String targetFormat,
String bitrate,
) {
- return 'Convert from $sourceFormat to $targetFormat at $bitrate?\n\nThe original file will be deleted after conversion.';
+ return 'Конвертировать из $sourceFormat в $targetFormat $bitrate?\n\nОригинальный файл будет удален после конвертации.';
}
@override
- String get trackConvertConverting => 'Converting audio...';
+ String get trackConvertConverting => 'Конвертация аудио...';
@override
String trackConvertSuccess(String format) {
- return 'Converted to $format successfully';
+ return 'Успешно конвертировано в $format';
}
@override
- String get trackConvertFailed => 'Conversion failed';
+ String get trackConvertFailed => 'Ошибка конвертации';
@override
- String get actionCreate => 'Create';
+ String get cueSplitTitle => 'Split CUE Sheet';
@override
- String get collectionFoldersTitle => 'My folders';
+ String get cueSplitSubtitle => 'Split CUE+FLAC into individual tracks';
@override
- String get collectionWishlist => 'Wishlist';
+ String cueSplitAlbum(String album) {
+ return 'Album: $album';
+ }
@override
- String get collectionLoved => 'Loved';
+ String cueSplitArtist(String artist) {
+ return 'Artist: $artist';
+ }
@override
- String get collectionPlaylists => 'Playlists';
+ String cueSplitTrackCount(int count) {
+ return '$count tracks';
+ }
@override
- String get collectionPlaylist => 'Playlist';
+ String get cueSplitConfirmTitle => 'Split CUE Album';
@override
- String get collectionAddToPlaylist => 'Add to playlist';
+ String cueSplitConfirmMessage(String album, int count) {
+ return 'Split \"$album\" into $count individual FLAC files?\n\nFiles will be saved to the same directory.';
+ }
@override
- String get collectionCreatePlaylist => 'Create playlist';
+ String cueSplitSplitting(int current, int total) {
+ return 'Splitting CUE sheet... ($current/$total)';
+ }
@override
- String get collectionNoPlaylistsYet => 'No playlists yet';
+ String cueSplitSuccess(int count) {
+ return 'Split into $count tracks successfully';
+ }
+
+ @override
+ String get cueSplitFailed => 'CUE split failed';
+
+ @override
+ String get cueSplitNoAudioFile => 'Audio file not found for this CUE sheet';
+
+ @override
+ String get cueSplitButton => 'Split into Tracks';
+
+ @override
+ String get actionCreate => 'Создать';
+
+ @override
+ String get collectionFoldersTitle => 'Мои папки';
+
+ @override
+ String get collectionWishlist => 'Список желаемого';
+
+ @override
+ String get collectionLoved => 'Любимые';
+
+ @override
+ String get collectionPlaylists => 'Плейлисты';
+
+ @override
+ String get collectionPlaylist => 'Плейлист';
+
+ @override
+ String get collectionAddToPlaylist => 'Добавить в плейлист';
+
+ @override
+ String get collectionCreatePlaylist => 'Создать плейлист';
+
+ @override
+ String get collectionNoPlaylistsYet => 'Плейлисты отсутствуют';
@override
String get collectionNoPlaylistsSubtitle =>
- 'Create a playlist to start categorizing tracks';
+ 'Создайте плейлист, чтобы начать классифицировать треки';
@override
String collectionPlaylistTracks(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
- other: '$count tracks',
- one: '1 track',
+ other: '$count треков',
+ many: '$count треков',
+ few: '$count трека',
+ one: '$count трек',
);
return '$_temp0';
}
@override
String collectionAddedToPlaylist(String playlistName) {
- return 'Added to \"$playlistName\"';
+ return 'Добавлено в \"$playlistName\"';
}
@override
String collectionAlreadyInPlaylist(String playlistName) {
- return 'Already in \"$playlistName\"';
+ return 'Уже в \"$playlistName\"';
}
@override
- String get collectionPlaylistCreated => 'Playlist created';
+ String get collectionPlaylistCreated => 'Плейлист создан';
@override
- String get collectionPlaylistNameHint => 'Playlist name';
+ String get collectionPlaylistNameHint => 'Название плейлиста';
@override
- String get collectionPlaylistNameRequired => 'Playlist name is required';
+ String get collectionPlaylistNameRequired => 'Имя плейлиста обязательно';
@override
- String get collectionRenamePlaylist => 'Rename playlist';
+ String get collectionRenamePlaylist => 'Переименовать плейлист';
@override
- String get collectionDeletePlaylist => 'Delete playlist';
+ String get collectionDeletePlaylist => 'Удалить плейлист';
@override
String collectionDeletePlaylistMessage(String playlistName) {
- return 'Delete \"$playlistName\" and all tracks inside it?';
+ return 'Удалить \"$playlistName\" и все треки внутри него?';
}
@override
- String get collectionPlaylistDeleted => 'Playlist deleted';
+ String get collectionPlaylistDeleted => 'Плейлист удалён';
@override
- String get collectionPlaylistRenamed => 'Playlist renamed';
+ String get collectionPlaylistRenamed => 'Плейлист переименован';
@override
- String get collectionWishlistEmptyTitle => 'Wishlist is empty';
+ String get collectionWishlistEmptyTitle => 'Список желаний пуст';
@override
String get collectionWishlistEmptySubtitle =>
- 'Tap + on tracks to save what you want to download later';
+ 'Нажмите + на треках, чтобы сохранить то, что вы хотите скачать позже';
@override
- String get collectionLovedEmptyTitle => 'Loved folder is empty';
+ String get collectionLovedEmptyTitle => 'Папка Любимые пуста';
@override
String get collectionLovedEmptySubtitle =>
- 'Tap love on tracks to keep your favorites';
+ 'Нажмите \"любовь\" на треках, чтобы сохранить ваши избранные';
@override
- String get collectionPlaylistEmptyTitle => 'Playlist is empty';
+ String get collectionPlaylistEmptyTitle => 'Плейлист пуст';
@override
String get collectionPlaylistEmptySubtitle =>
- 'Long-press + on any track to add it here';
+ 'Удерживайте + на любом треке, чтобы добавить его сюда';
@override
- String get collectionRemoveFromPlaylist => 'Remove from playlist';
+ String get collectionRemoveFromPlaylist => 'Удалить из плейлиста';
@override
- String get collectionRemoveFromFolder => 'Remove from folder';
+ String get collectionRemoveFromFolder => 'Убрать из папки';
@override
String collectionRemoved(String trackName) {
- return '\"$trackName\" removed';
+ return '\"$trackName\" удалён';
}
@override
String collectionAddedToLoved(String trackName) {
- return '\"$trackName\" added to Loved';
+ return '\"$trackName\" добавлен в Любимые';
}
@override
String collectionRemovedFromLoved(String trackName) {
- return '\"$trackName\" removed from Loved';
+ return '\"$trackName\" удалено из Любимых';
}
@override
String collectionAddedToWishlist(String trackName) {
- return '\"$trackName\" added to Wishlist';
+ return '\"$trackName\" добавлен в список желаний';
}
@override
String collectionRemovedFromWishlist(String trackName) {
- return '\"$trackName\" removed from Wishlist';
+ return '\"$trackName\" удалён из списка желаний';
}
@override
- String get trackOptionAddToLoved => 'Add to Loved';
+ String get trackOptionAddToLoved => 'Добавить в Любимое';
@override
- String get trackOptionRemoveFromLoved => 'Remove from Loved';
+ String get trackOptionRemoveFromLoved => 'Исключить из Любимых';
@override
- String get trackOptionAddToWishlist => 'Add to Wishlist';
+ String get trackOptionAddToWishlist => 'Добавить в список желаний';
@override
- String get trackOptionRemoveFromWishlist => 'Remove from Wishlist';
+ String get trackOptionRemoveFromWishlist => 'Удалить из списка желаний';
@override
- String get collectionPlaylistChangeCover => 'Change cover image';
+ String get collectionPlaylistChangeCover => 'Изменить обложку';
@override
- String get collectionPlaylistRemoveCover => 'Remove cover image';
+ String get collectionPlaylistRemoveCover => 'Удалить обложку';
@override
String selectionShareCount(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
- other: 'tracks',
- one: 'track',
+ other: 'треков',
+ many: 'треков',
+ few: 'трека',
+ one: 'трек',
);
- return 'Share $count $_temp0';
+ return 'Отправить $count $_temp0';
}
@override
@@ -2343,17 +2416,19 @@ class AppLocalizationsRu extends AppLocalizations {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
- other: 'tracks',
- one: 'track',
+ other: 'треков',
+ many: 'треков',
+ few: 'трека',
+ one: 'трек',
);
- return 'Convert $count $_temp0';
+ return 'Конвертировать $count $_temp0';
}
@override
- String get selectionConvertNoConvertible => 'No convertible tracks selected';
+ String get selectionConvertNoConvertible => 'Не выбраны конвертируемые треки';
@override
- String get selectionBatchConvertConfirmTitle => 'Batch Convert';
+ String get selectionBatchConvertConfirmTitle => 'Пакетная конвертация';
@override
String selectionBatchConvertConfirmMessage(
@@ -2372,12 +2447,12 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String selectionBatchConvertProgress(int current, int total) {
- return 'Converting $current of $total...';
+ return 'Конвертация $current из $total...';
}
@override
String selectionBatchConvertSuccess(int success, int total, String format) {
- return 'Converted $success of $total tracks to $format';
+ return 'Конвертировано $success треков $total в $format';
}
@override
@@ -2387,7 +2462,7 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get downloadUseAlbumArtistForFoldersAlbumSubtitle =>
- 'Artist folders use Album Artist when available';
+ 'Для папок исполнителей используется исполнитель альбома, если он указан';
@override
String get downloadUseAlbumArtistForFoldersTrackSubtitle =>
diff --git a/lib/l10n/app_localizations_tr.dart b/lib/l10n/app_localizations_tr.dart
index bcc77961..3a88e179 100644
--- a/lib/l10n/app_localizations_tr.dart
+++ b/lib/l10n/app_localizations_tr.dart
@@ -361,7 +361,7 @@ class AppLocalizationsTr extends AppLocalizations {
@override
String get aboutAppDescription =>
- 'Spotify şarkılarını Tidal, Qobuz ve Amazon Music\'den yüksek kalitede indir.';
+ 'Spotify şarkılarını Tidal ve Qobuz\'den yüksek kalitede indir.';
@override
String get artistAlbums => 'Albümler';
@@ -693,6 +693,17 @@ class AppLocalizationsTr extends AppLocalizations {
@override
String get errorNoTracksFound => 'Parça bulunamadı';
+ @override
+ String get errorUrlNotRecognized => 'Link not recognized';
+
+ @override
+ String get errorUrlNotRecognizedMessage =>
+ 'This link is not supported. Make sure the URL is correct and a compatible extension is installed.';
+
+ @override
+ String get errorUrlFetchFailed =>
+ 'Failed to load content from this link. Please try again.';
+
@override
String errorMissingExtensionSource(String item) {
return '$item yüklenemedi: Eksik eklenti kaynağı';
@@ -766,6 +777,13 @@ class AppLocalizationsTr extends AppLocalizations {
@override
String get folderOrganizationNone => 'Organizasyon yok';
+ @override
+ String get folderOrganizationByPlaylist => 'By Playlist';
+
+ @override
+ String get folderOrganizationByPlaylistSubtitle =>
+ 'Separate folder for each playlist';
+
@override
String get folderOrganizationByArtist => 'Sanatçıya Göre';
@@ -1821,7 +1839,7 @@ class AppLocalizationsTr extends AppLocalizations {
@override
String get tutorialWelcomeTip2 =>
- 'Get FLAC quality audio from Tidal, Qobuz, or Amazon Music';
+ 'Tidal, Qobuz veya Deezer\'den FLAC kalitesinde ses alın';
@override
String get tutorialWelcomeTip3 =>
@@ -2138,6 +2156,54 @@ class AppLocalizationsTr extends AppLocalizations {
@override
String get trackConvertFailed => 'Conversion failed';
+ @override
+ String get cueSplitTitle => 'Split CUE Sheet';
+
+ @override
+ String get cueSplitSubtitle => 'Split CUE+FLAC into individual tracks';
+
+ @override
+ String cueSplitAlbum(String album) {
+ return 'Album: $album';
+ }
+
+ @override
+ String cueSplitArtist(String artist) {
+ return 'Artist: $artist';
+ }
+
+ @override
+ String cueSplitTrackCount(int count) {
+ return '$count tracks';
+ }
+
+ @override
+ String get cueSplitConfirmTitle => 'Split CUE Album';
+
+ @override
+ String cueSplitConfirmMessage(String album, int count) {
+ return 'Split \"$album\" into $count individual FLAC files?\n\nFiles will be saved to the same directory.';
+ }
+
+ @override
+ String cueSplitSplitting(int current, int total) {
+ return 'Splitting CUE sheet... ($current/$total)';
+ }
+
+ @override
+ String cueSplitSuccess(int count) {
+ return 'Split into $count tracks successfully';
+ }
+
+ @override
+ String get cueSplitFailed => 'CUE split failed';
+
+ @override
+ String get cueSplitNoAudioFile => 'Audio file not found for this CUE sheet';
+
+ @override
+ String get cueSplitButton => 'Split into Tracks';
+
@override
String get actionCreate => 'Create';
diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart
index 433e771b..3306bb1c 100644
--- a/lib/l10n/app_localizations_zh.dart
+++ b/lib/l10n/app_localizations_zh.dart
@@ -350,6 +350,2412 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get aboutSpotiSaver => 'SpotiSaver';
+ @override
+ String get aboutSpotiSaverDesc =>
+ 'Tidal Hi-Res FLAC streaming endpoints. A key piece of the lossless puzzle!';
+
+ @override
+ String get aboutAppDescription =>
+ 'Download Spotify tracks in lossless quality from Tidal and Qobuz.';
+
+ @override
+ String get artistAlbums => 'Albums';
+
+ @override
+ String get artistSingles => 'Singles & EPs';
+
+ @override
+ String get artistCompilations => 'Compilations';
+
+ @override
+ String get artistPopular => 'Popular';
+
+ @override
+ String artistMonthlyListeners(String count) {
+ return '$count monthly listeners';
+ }
+
+ @override
+ String get trackMetadataService => 'Service';
+
+ @override
+ String get trackMetadataPlay => 'Play';
+
+ @override
+ String get trackMetadataShare => 'Share';
+
+ @override
+ String get trackMetadataDelete => 'Delete';
+
+ @override
+ String get setupGrantPermission => 'Grant Permission';
+
+ @override
+ String get setupSkip => 'Skip for now';
+
+ @override
+ String get setupStorageAccessRequired => 'Storage Access Required';
+
+ @override
+ String get setupStorageAccessMessageAndroid11 =>
+ 'Android 11+ requires \"All files access\" permission to save files to your chosen download folder.';
+
+ @override
+ String get setupOpenSettings => 'Open Settings';
+
+ @override
+ String get setupPermissionDeniedMessage =>
+ 'Permission denied. Please grant all permissions to continue.';
+
+ @override
+ String setupPermissionRequired(String permissionType) {
+ return '$permissionType Permission Required';
+ }
+
+ @override
+ String setupPermissionRequiredMessage(String permissionType) {
+ return '$permissionType permission is required for the best experience. You can change this later in Settings.';
+ }
+
+ @override
+ String get setupUseDefaultFolder => 'Use Default Folder?';
+
+ @override
+ String get setupNoFolderSelected =>
+ 'No folder selected. Would you like to use the default Music folder?';
+
+ @override
+ String get setupUseDefault => 'Use Default';
+
+ @override
+ String get setupDownloadLocationTitle => 'Download Location';
+
+ @override
+ String get setupDownloadLocationIosMessage =>
+ 'On iOS, downloads are saved to the app\'s Documents folder. You can access them via the Files app.';
+
+ @override
+ String get setupAppDocumentsFolder => 'App Documents Folder';
+
+ @override
+ String get setupAppDocumentsFolderSubtitle =>
+ 'Recommended - accessible via Files app';
+
+ @override
+ String get setupChooseFromFiles => 'Choose from Files';
+
+ @override
+ String get setupChooseFromFilesSubtitle => 'Select iCloud or other location';
+
+ @override
+ String get setupIosEmptyFolderWarning =>
+ 'iOS limitation: Empty folders cannot be selected. Choose a folder with at least one file.';
+
+ @override
+ String get setupIcloudNotSupported =>
+ 'iCloud Drive is not supported. Please use the app Documents folder.';
+
+ @override
+ String get setupDownloadInFlac => 'Download Spotify tracks in FLAC';
+
+ @override
+ String get setupStorageGranted => 'Storage Permission Granted!';
+
+ @override
+ String get setupStorageRequired => 'Storage Permission Required';
+
+ @override
+ String get setupStorageDescription =>
+ 'SpotiFLAC needs storage permission to save your downloaded music files.';
+
+ @override
+ String get setupNotificationGranted => 'Notification Permission Granted!';
+
+ @override
+ String get setupNotificationEnable => 'Enable Notifications';
+
+ @override
+ String get setupFolderChoose => 'Choose Download Folder';
+
+ @override
+ String get setupFolderDescription =>
+ 'Select a folder where your downloaded music will be saved.';
+
+ @override
+ String get setupSelectFolder => 'Select Folder';
+
+ @override
+ String get setupEnableNotifications => 'Enable Notifications';
+
+ @override
+ String get setupNotificationBackgroundDescription =>
+ 'Get notified about download progress and completion. This helps you track downloads when the app is in background.';
+
+ @override
+ String get setupSkipForNow => 'Skip for now';
+
+ @override
+ String get setupNext => 'Next';
+
+ @override
+ String get setupGetStarted => 'Get Started';
+
+ @override
+ String get setupAllowAccessToManageFiles =>
+ 'Please enable \"Allow access to manage all files\" in the next screen.';
+
+ @override
+ String get dialogCancel => 'Cancel';
+
+ @override
+ String get dialogSave => 'Save';
+
+ @override
+ String get dialogDelete => 'Delete';
+
+ @override
+ String get dialogRetry => 'Retry';
+
+ @override
+ String get dialogClear => 'Clear';
+
+ @override
+ String get dialogDone => 'Done';
+
+ @override
+ String get dialogImport => 'Import';
+
+ @override
+ String get dialogDiscard => 'Discard';
+
+ @override
+ String get dialogRemove => 'Remove';
+
+ @override
+ String get dialogUninstall => 'Uninstall';
+
+ @override
+ String get dialogDiscardChanges => 'Discard Changes?';
+
+ @override
+ String get dialogUnsavedChanges =>
+ 'You have unsaved changes. Do you want to discard them?';
+
+ @override
+ String get dialogClearAll => 'Clear All';
+
+ @override
+ String get dialogRemoveExtension => 'Remove Extension';
+
+ @override
+ String get dialogRemoveExtensionMessage =>
+ 'Are you sure you want to remove this extension? This cannot be undone.';
+
+ @override
+ String get dialogUninstallExtension => 'Uninstall Extension?';
+
+ @override
+ String dialogUninstallExtensionMessage(String extensionName) {
+ return 'Are you sure you want to remove $extensionName?';
+ }
+
+ @override
+ String get dialogClearHistoryTitle => 'Clear History';
+
+ @override
+ String get dialogClearHistoryMessage =>
+ 'Are you sure you want to clear all download history? This cannot be undone.';
+
+ @override
+ String get dialogDeleteSelectedTitle => 'Delete Selected';
+
+ @override
+ String dialogDeleteSelectedMessage(int count) {
+ String _temp0 = intl.Intl.pluralLogic(
+ count,
+ locale: localeName,
+ other: 'tracks',
+ one: 'track',
+ );
+ return 'Delete $count $_temp0 from history?\n\nThis will also delete the files from storage.';
+ }
+
+ @override
+ String get dialogImportPlaylistTitle => 'Import Playlist';
+
+ @override
+ String dialogImportPlaylistMessage(int count) {
+ return 'Found $count tracks in CSV. Add them to download queue?';
+ }
+
+ @override
+ String csvImportTracks(int count) {
+ return '$count tracks from CSV';
+ }
+
+ @override
+ String snackbarAddedToQueue(String trackName) {
+ return 'Added \"$trackName\" to queue';
+ }
+
+ @override
+ String snackbarAddedTracksToQueue(int count) {
+ return 'Added $count tracks to queue';
+ }
+
+ @override
+ String snackbarAlreadyDownloaded(String trackName) {
+ return '\"$trackName\" already downloaded';
+ }
+
+ @override
+ String snackbarAlreadyInLibrary(String trackName) {
+ return '\"$trackName\" already exists in your library';
+ }
+
+ @override
+ String get snackbarHistoryCleared => 'History cleared';
+
+ @override
+ String get snackbarCredentialsSaved => 'Credentials saved';
+
+ @override
+ String get snackbarCredentialsCleared => 'Credentials cleared';
+
+ @override
+ String snackbarDeletedTracks(int count) {
+ String _temp0 = intl.Intl.pluralLogic(
+ count,
+ locale: localeName,
+ other: 'tracks',
+ one: 'track',
+ );
+ return 'Deleted $count $_temp0';
+ }
+
+ @override
+ String snackbarCannotOpenFile(String error) {
+ return 'Cannot open file: $error';
+ }
+
+ @override
+ String get snackbarFillAllFields => 'Please fill all fields';
+
+ @override
+ String get snackbarViewQueue => 'View Queue';
+
+ @override
+ String snackbarUrlCopied(String platform) {
+ return '$platform URL copied to clipboard';
+ }
+
+ @override
+ String get snackbarFileNotFound => 'File not found';
+
+ @override
+ String get snackbarSelectExtFile => 'Please select a .spotiflac-ext file';
+
+ @override
+ String get snackbarProviderPrioritySaved => 'Provider priority saved';
+
+ @override
+ String get snackbarMetadataProviderSaved =>
+ 'Metadata provider priority saved';
+
+ @override
+ String snackbarExtensionInstalled(String extensionName) {
+ return '$extensionName installed.';
+ }
+
+ @override
+ String snackbarExtensionUpdated(String extensionName) {
+ return '$extensionName updated.';
+ }
+
+ @override
+ String get snackbarFailedToInstall => 'Failed to install extension';
+
+ @override
+ String get snackbarFailedToUpdate => 'Failed to update extension';
+
+ @override
+ String get errorRateLimited => 'Rate Limited';
+
+ @override
+ String get errorRateLimitedMessage =>
+ 'Too many requests. Please wait a moment before searching again.';
+
+ @override
+ String get errorNoTracksFound => 'No tracks found';
+
+ @override
+ String get errorUrlNotRecognized => 'Link not recognized';
+
+ @override
+ String get errorUrlNotRecognizedMessage =>
+ 'This link is not supported. Make sure the URL is correct and a compatible extension is installed.';
+
+ @override
+ String get errorUrlFetchFailed =>
+ 'Failed to load content from this link. Please try again.';
+
+ @override
+ String errorMissingExtensionSource(String item) {
+ return 'Cannot load $item: missing extension source';
+ }
+
+ @override
+ String get actionPause => 'Pause';
+
+ @override
+ String get actionResume => 'Resume';
+
+ @override
+ String get actionCancel => 'Cancel';
+
+ @override
+ String get actionSelectAll => 'Select All';
+
+ @override
+ String get actionDeselect => 'Deselect';
+
+ @override
+ String get actionRemoveCredentials => 'Remove Credentials';
+
+ @override
+ String get actionSaveCredentials => 'Save Credentials';
+
+ @override
+ String selectionSelected(int count) {
+ return '$count selected';
+ }
+
+ @override
+ String get selectionAllSelected => 'All tracks selected';
+
+ @override
+ String get selectionSelectToDelete => 'Select tracks to delete';
+
+ @override
+ String progressFetchingMetadata(int current, int total) {
+ return 'Fetching metadata... $current/$total';
+ }
+
+ @override
+ String get progressReadingCsv => 'Reading CSV...';
+
+ @override
+ String get searchSongs => 'Songs';
+
+ @override
+ String get searchArtists => 'Artists';
+
+ @override
+ String get searchAlbums => 'Albums';
+
+ @override
+ String get searchPlaylists => 'Playlists';
+
+ @override
+ String get tooltipPlay => 'Play';
+
+ @override
+ String get filenameFormat => 'Filename Format';
+
+ @override
+ String get filenameShowAdvancedTags => 'Show advanced tags';
+
+ @override
+ String get filenameShowAdvancedTagsDescription =>
+ 'Enable formatted tags for track padding and date patterns';
+
+ @override
+ String get folderOrganizationNone => 'No organization';
+
+ @override
+ String get folderOrganizationByPlaylist => 'By Playlist';
+
+ @override
+ String get folderOrganizationByPlaylistSubtitle =>
+ 'Separate folder for each playlist';
+
+ @override
+ String get folderOrganizationByArtist => 'By Artist';
+
+ @override
+ String get folderOrganizationByAlbum => 'By Album';
+
+ @override
+ String get folderOrganizationByArtistAlbum => 'Artist/Album';
+
+ @override
+ String get folderOrganizationDescription =>
+ 'Organize downloaded files into folders';
+
+ @override
+ String get folderOrganizationNoneSubtitle => 'All files in download folder';
+
+ @override
+ String get folderOrganizationByArtistSubtitle =>
+ 'Separate folder for each artist';
+
+ @override
+ String get folderOrganizationByAlbumSubtitle =>
+ 'Separate folder for each album';
+
+ @override
+ String get folderOrganizationByArtistAlbumSubtitle =>
+ 'Nested folders for artist and album';
+
+ @override
+ String get updateAvailable => 'Update Available';
+
+ @override
+ String get updateLater => 'Later';
+
+ @override
+ String get updateStartingDownload => 'Starting download...';
+
+ @override
+ String get updateDownloadFailed => 'Download failed';
+
+ @override
+ String get updateFailedMessage => 'Failed to download update';
+
+ @override
+ String get updateNewVersionReady => 'A new version is ready';
+
+ @override
+ String get updateCurrent => 'Current';
+
+ @override
+ String get updateNew => 'New';
+
+ @override
+ String get updateDownloading => 'Downloading...';
+
+ @override
+ String get updateWhatsNew => 'What\'s New';
+
+ @override
+ String get updateDownloadInstall => 'Download & Install';
+
+ @override
+ String get updateDontRemind => 'Don\'t remind';
+
+ @override
+ String get providerPriorityTitle => 'Provider Priority';
+
+ @override
+ String get providerPriorityDescription =>
+ 'Drag to reorder download providers. The app will try providers from top to bottom when downloading tracks.';
+
+ @override
+ String get providerPriorityInfo =>
+ 'If a track is not available on the first provider, the app will automatically try the next one.';
+
+ @override
+ String get providerBuiltIn => 'Built-in';
+
+ @override
+ String get providerExtension => 'Extension';
+
+ @override
+ String get metadataProviderPriorityTitle => 'Metadata Priority';
+
+ @override
+ String get metadataProviderPriorityDescription =>
+ 'Drag to reorder metadata providers. The app will try providers from top to bottom when searching for tracks and fetching metadata.';
+
+ @override
+ String get metadataProviderPriorityInfo =>
+ 'Deezer has no rate limits and is recommended as primary. Spotify may rate limit after many requests.';
+
+ @override
+ String get metadataNoRateLimits => 'No rate limits';
+
+ @override
+ String get metadataMayRateLimit => 'May rate limit';
+
+ @override
+ String get logTitle => 'Logs';
+
+ @override
+ String get logCopied => 'Logs copied to clipboard';
+
+ @override
+ String get logSearchHint => 'Search logs...';
+
+ @override
+ String get logFilterLevel => 'Level';
+
+ @override
+ String get logFilterSection => 'Filter';
+
+ @override
+ String get logShareLogs => 'Share logs';
+
+ @override
+ String get logClearLogs => 'Clear logs';
+
+ @override
+ String get logClearLogsTitle => 'Clear Logs';
+
+ @override
+ String get logClearLogsMessage => 'Are you sure you want to clear all logs?';
+
+ @override
+ String get logFilterBySeverity => 'Filter logs by severity';
+
+ @override
+ String get logNoLogsYet => 'No logs yet';
+
+ @override
+ String get logNoLogsYetSubtitle => 'Logs will appear here as you use the app';
+
+ @override
+ String logEntriesFiltered(int count) {
+ return 'Entries ($count filtered)';
+ }
+
+ @override
+ String logEntries(int count) {
+ return 'Entries ($count)';
+ }
+
+ @override
+ String get credentialsTitle => 'Spotify Credentials';
+
+ @override
+ String get credentialsDescription =>
+ 'Enter your Client ID and Secret to use your own Spotify application quota.';
+
+ @override
+ String get credentialsClientId => 'Client ID';
+
+ @override
+ String get credentialsClientIdHint => 'Paste Client ID';
+
+ @override
+ String get credentialsClientSecret => 'Client Secret';
+
+ @override
+ String get credentialsClientSecretHint => 'Paste Client Secret';
+
+ @override
+ String get channelStable => 'Stable';
+
+ @override
+ String get channelPreview => 'Preview';
+
+ @override
+ String get sectionSearchSource => 'Search Source';
+
+ @override
+ String get sectionDownload => 'Download';
+
+ @override
+ String get sectionPerformance => 'Performance';
+
+ @override
+ String get sectionApp => 'App';
+
+ @override
+ String get sectionData => 'Data';
+
+ @override
+ String get sectionDebug => 'Debug';
+
+ @override
+ String get sectionService => 'Service';
+
+ @override
+ String get sectionAudioQuality => 'Audio Quality';
+
+ @override
+ String get sectionFileSettings => 'File Settings';
+
+ @override
+ String get sectionLyrics => 'Lyrics';
+
+ @override
+ String get lyricsMode => 'Lyrics Mode';
+
+ @override
+ String get lyricsModeDescription =>
+ 'Choose how lyrics are saved with your downloads';
+
+ @override
+ String get lyricsModeEmbed => 'Embed in file';
+
+ @override
+ String get lyricsModeEmbedSubtitle => 'Lyrics stored inside FLAC metadata';
+
+ @override
+ String get lyricsModeExternal => 'External .lrc file';
+
+ @override
+ String get lyricsModeExternalSubtitle =>
+ 'Separate .lrc file for players like Samsung Music';
+
+ @override
+ String get lyricsModeBoth => 'Both';
+
+ @override
+ String get lyricsModeBothSubtitle => 'Embed and save .lrc file';
+
+ @override
+ String get sectionColor => 'Color';
+
+ @override
+ String get sectionTheme => 'Theme';
+
+ @override
+ String get sectionLayout => 'Layout';
+
+ @override
+ String get sectionLanguage => 'Language';
+
+ @override
+ String get appearanceLanguage => 'App Language';
+
+ @override
+ String get settingsAppearanceSubtitle => 'Theme, colors, display';
+
+ @override
+ String get settingsDownloadSubtitle => 'Service, quality, filename format';
+
+ @override
+ String get settingsOptionsSubtitle => 'Fallback, lyrics, cover art, updates';
+
+ @override
+ String get settingsExtensionsSubtitle => 'Manage download providers';
+
+ @override
+ String get settingsLogsSubtitle => 'View app logs for debugging';
+
+ @override
+ String get loadingSharedLink => 'Loading shared link...';
+
+ @override
+ String get pressBackAgainToExit => 'Press back again to exit';
+
+ @override
+ String downloadAllCount(int count) {
+ return 'Download All ($count)';
+ }
+
+ @override
+ String tracksCount(int count) {
+ String _temp0 = intl.Intl.pluralLogic(
+ count,
+ locale: localeName,
+ other: '$count tracks',
+ one: '1 track',
+ );
+ return '$_temp0';
+ }
+
+ @override
+ String get trackCopyFilePath => 'Copy file path';
+
+ @override
+ String get trackRemoveFromDevice => 'Remove from device';
+
+ @override
+ String get trackLoadLyrics => 'Load Lyrics';
+
+ @override
+ String get trackMetadata => 'Metadata';
+
+ @override
+ String get trackFileInfo => 'File Info';
+
+ @override
+ String get trackLyrics => 'Lyrics';
+
+ @override
+ String get trackFileNotFound => 'File not found';
+
+ @override
+ String get trackOpenInDeezer => 'Open in Deezer';
+
+ @override
+ String get trackOpenInSpotify => 'Open in Spotify';
+
+ @override
+ String get trackTrackName => 'Track name';
+
+ @override
+ String get trackArtist => 'Artist';
+
+ @override
+ String get trackAlbumArtist => 'Album artist';
+
+ @override
+ String get trackAlbum => 'Album';
+
+ @override
+ String get trackTrackNumber => 'Track number';
+
+ @override
+ String get trackDiscNumber => 'Disc number';
+
+ @override
+ String get trackDuration => 'Duration';
+
+ @override
+ String get trackAudioQuality => 'Audio quality';
+
+ @override
+ String get trackReleaseDate => 'Release date';
+
+ @override
+ String get trackGenre => 'Genre';
+
+ @override
+ String get trackLabel => 'Label';
+
+ @override
+ String get trackCopyright => 'Copyright';
+
+ @override
+ String get trackDownloaded => 'Downloaded';
+
+ @override
+ String get trackCopyLyrics => 'Copy lyrics';
+
+ @override
+ String get trackLyricsNotAvailable => 'Lyrics not available for this track';
+
+ @override
+ String get trackLyricsTimeout => 'Request timed out. Try again later.';
+
+ @override
+ String get trackLyricsLoadFailed => 'Failed to load lyrics';
+
+ @override
+ String get trackEmbedLyrics => 'Embed Lyrics';
+
+ @override
+ String get trackLyricsEmbedded => 'Lyrics embedded successfully';
+
+ @override
+ String get trackInstrumental => 'Instrumental track';
+
+ @override
+ String get trackCopiedToClipboard => 'Copied to clipboard';
+
+ @override
+ String get trackDeleteConfirmTitle => 'Remove from device?';
+
+ @override
+ String get trackDeleteConfirmMessage =>
+ 'This will permanently delete the downloaded file and remove it from your history.';
+
+ @override
+ String get dateToday => 'Today';
+
+ @override
+ String get dateYesterday => 'Yesterday';
+
+ @override
+ String dateDaysAgo(int count) {
+ return '$count days ago';
+ }
+
+ @override
+ String dateWeeksAgo(int count) {
+ return '$count weeks ago';
+ }
+
+ @override
+ String dateMonthsAgo(int count) {
+ return '$count months ago';
+ }
+
+ @override
+ String get storeFilterAll => 'All';
+
+ @override
+ String get storeFilterMetadata => 'Metadata';
+
+ @override
+ String get storeFilterDownload => 'Download';
+
+ @override
+ String get storeFilterUtility => 'Utility';
+
+ @override
+ String get storeFilterLyrics => 'Lyrics';
+
+ @override
+ String get storeFilterIntegration => 'Integration';
+
+ @override
+ String get storeClearFilters => 'Clear filters';
+
+ @override
+ String get extensionDefaultProvider => 'Default (Deezer/Spotify)';
+
+ @override
+ String get extensionDefaultProviderSubtitle => 'Use built-in search';
+
+ @override
+ String get extensionAuthor => 'Author';
+
+ @override
+ String get extensionId => 'ID';
+
+ @override
+ String get extensionError => 'Error';
+
+ @override
+ String get extensionCapabilities => 'Capabilities';
+
+ @override
+ String get extensionMetadataProvider => 'Metadata Provider';
+
+ @override
+ String get extensionDownloadProvider => 'Download Provider';
+
+ @override
+ String get extensionLyricsProvider => 'Lyrics Provider';
+
+ @override
+ String get extensionUrlHandler => 'URL Handler';
+
+ @override
+ String get extensionQualityOptions => 'Quality Options';
+
+ @override
+ String get extensionPostProcessingHooks => 'Post-Processing Hooks';
+
+ @override
+ String get extensionPermissions => 'Permissions';
+
+ @override
+ String get extensionSettings => 'Settings';
+
+ @override
+ String get extensionRemoveButton => 'Remove Extension';
+
+ @override
+ String get extensionUpdated => 'Updated';
+
+ @override
+ String get extensionMinAppVersion => 'Min App Version';
+
+ @override
+ String get extensionCustomTrackMatching => 'Custom Track Matching';
+
+ @override
+ String get extensionPostProcessing => 'Post-Processing';
+
+ @override
+ String extensionHooksAvailable(int count) {
+ return '$count hook(s) available';
+ }
+
+ @override
+ String extensionPatternsCount(int count) {
+ return '$count pattern(s)';
+ }
+
+ @override
+ String extensionStrategy(String strategy) {
+ return 'Strategy: $strategy';
+ }
+
+ @override
+ String get extensionsProviderPrioritySection => 'Provider Priority';
+
+ @override
+ String get extensionsInstalledSection => 'Installed Extensions';
+
+ @override
+ String get extensionsNoExtensions => 'No extensions installed';
+
+ @override
+ String get extensionsNoExtensionsSubtitle =>
+ 'Install .spotiflac-ext files to add new providers';
+
+ @override
+ String get extensionsInstallButton => 'Install Extension';
+
+ @override
+ String get extensionsInfoTip =>
+ 'Extensions can add new metadata and download providers. Only install extensions from trusted sources.';
+
+ @override
+ String get extensionsInstalledSuccess => 'Extension installed successfully';
+
+ @override
+ String get extensionsDownloadPriority => 'Download Priority';
+
+ @override
+ String get extensionsDownloadPrioritySubtitle => 'Set download service order';
+
+ @override
+ String get extensionsNoDownloadProvider =>
+ 'No extensions with download provider';
+
+ @override
+ String get extensionsMetadataPriority => 'Metadata Priority';
+
+ @override
+ String get extensionsMetadataPrioritySubtitle =>
+ 'Set search & metadata source order';
+
+ @override
+ String get extensionsNoMetadataProvider =>
+ 'No extensions with metadata provider';
+
+ @override
+ String get extensionsSearchProvider => 'Search Provider';
+
+ @override
+ String get extensionsNoCustomSearch => 'No extensions with custom search';
+
+ @override
+ String get extensionsSearchProviderDescription =>
+ 'Choose which service to use for searching tracks';
+
+ @override
+ String get extensionsCustomSearch => 'Custom search';
+
+ @override
+ String get extensionsErrorLoading => 'Error loading extension';
+
+ @override
+ String get qualityFlacLossless => 'FLAC Lossless';
+
+ @override
+ String get qualityFlacLosslessSubtitle => '16-bit / 44.1kHz';
+
+ @override
+ String get qualityHiResFlac => 'Hi-Res FLAC';
+
+ @override
+ String get qualityHiResFlacSubtitle => '24-bit / up to 96kHz';
+
+ @override
+ String get qualityHiResFlacMax => 'Hi-Res FLAC Max';
+
+ @override
+ String get qualityHiResFlacMaxSubtitle => '24-bit / up to 192kHz';
+
+ @override
+ String get qualityNote =>
+ 'Actual quality depends on track availability from the service';
+
+ @override
+ String get youtubeQualityNote =>
+ 'YouTube provides lossy audio only. Not part of lossless fallback.';
+
+ @override
+ String get youtubeOpusBitrateTitle => 'YouTube Opus Bitrate';
+
+ @override
+ String get youtubeMp3BitrateTitle => 'YouTube MP3 Bitrate';
+
+ @override
+ String get downloadAskBeforeDownload => 'Ask Before Download';
+
+ @override
+ String get downloadDirectory => 'Download Directory';
+
+ @override
+ String get downloadSeparateSinglesFolder => 'Separate Singles Folder';
+
+ @override
+ String get downloadAlbumFolderStructure => 'Album Folder Structure';
+
+ @override
+ String get downloadUseAlbumArtistForFolders => 'Use Album Artist for folders';
+
+ @override
+ String get downloadUsePrimaryArtistOnly => 'Primary artist only for folders';
+
+ @override
+ String get downloadUsePrimaryArtistOnlyEnabled =>
+ 'Featured artists removed from folder name (e.g. Justin Bieber, Quavo → Justin Bieber)';
+
+ @override
+ String get downloadUsePrimaryArtistOnlyDisabled =>
+ 'Full artist string used for folder name';
+
+ @override
+ String get downloadSelectQuality => 'Select Quality';
+
+ @override
+ String get downloadFrom => 'Download From';
+
+ @override
+ String get appearanceAmoledDark => 'AMOLED Dark';
+
+ @override
+ String get appearanceAmoledDarkSubtitle => 'Pure black background';
+
+ @override
+ String get queueClearAll => 'Clear All';
+
+ @override
+ String get queueClearAllMessage =>
+ 'Are you sure you want to clear all downloads?';
+
+ @override
+ String get settingsAutoExportFailed => 'Auto-export failed downloads';
+
+ @override
+ String get settingsAutoExportFailedSubtitle =>
+ 'Save failed downloads to TXT file automatically';
+
+ @override
+ String get settingsDownloadNetwork => 'Download Network';
+
+ @override
+ String get settingsDownloadNetworkAny => 'WiFi + Mobile Data';
+
+ @override
+ String get settingsDownloadNetworkWifiOnly => 'WiFi Only';
+
+ @override
+ String get settingsDownloadNetworkSubtitle =>
+ 'Choose which network to use for downloads. When set to WiFi Only, downloads will pause on mobile data.';
+
+ @override
+ String get albumFolderArtistAlbum => 'Artist / Album';
+
+ @override
+ String get albumFolderArtistAlbumSubtitle => 'Albums/Artist Name/Album Name/';
+
+ @override
+ String get albumFolderArtistYearAlbum => 'Artist / [Year] Album';
+
+ @override
+ String get albumFolderArtistYearAlbumSubtitle =>
+ 'Albums/Artist Name/[2005] Album Name/';
+
+ @override
+ String get albumFolderAlbumOnly => 'Album Only';
+
+ @override
+ String get albumFolderAlbumOnlySubtitle => 'Albums/Album Name/';
+
+ @override
+ String get albumFolderYearAlbum => '[Year] Album';
+
+ @override
+ String get albumFolderYearAlbumSubtitle => 'Albums/[2005] Album Name/';
+
+ @override
+ String get albumFolderArtistAlbumSingles => 'Artist / Album + Singles';
+
+ @override
+ String get albumFolderArtistAlbumSinglesSubtitle =>
+ 'Artist/Album/ and Artist/Singles/';
+
+ @override
+ String get downloadedAlbumDeleteSelected => 'Delete Selected';
+
+ @override
+ String downloadedAlbumDeleteMessage(int count) {
+ String _temp0 = intl.Intl.pluralLogic(
+ count,
+ locale: localeName,
+ other: 'tracks',
+ one: 'track',
+ );
+ return 'Delete $count $_temp0 from this album?\n\nThis will also delete the files from storage.';
+ }
+
+ @override
+ String downloadedAlbumSelectedCount(int count) {
+ return '$count selected';
+ }
+
+ @override
+ String get downloadedAlbumAllSelected => 'All tracks selected';
+
+ @override
+ String get downloadedAlbumTapToSelect => 'Tap tracks to select';
+
+ @override
+ String downloadedAlbumDeleteCount(int count) {
+ String _temp0 = intl.Intl.pluralLogic(
+ count,
+ locale: localeName,
+ other: 'tracks',
+ one: 'track',
+ );
+ return 'Delete $count $_temp0';
+ }
+
+ @override
+ String get downloadedAlbumSelectToDelete => 'Select tracks to delete';
+
+ @override
+ String downloadedAlbumDiscHeader(int discNumber) {
+ return 'Disc $discNumber';
+ }
+
+ @override
+ String get recentTypeArtist => 'Artist';
+
+ @override
+ String get recentTypeAlbum => 'Album';
+
+ @override
+ String get recentTypeSong => 'Song';
+
+ @override
+ String get recentTypePlaylist => 'Playlist';
+
+ @override
+ String get recentEmpty => 'No recent items yet';
+
+ @override
+ String get recentShowAllDownloads => 'Show All Downloads';
+
+ @override
+ String recentPlaylistInfo(String name) {
+ return 'Playlist: $name';
+ }
+
+ @override
+ String get discographyDownload => 'Download Discography';
+
+ @override
+ String get discographyDownloadAll => 'Download All';
+
+ @override
+ String discographyDownloadAllSubtitle(int count, int albumCount) {
+ return '$count tracks from $albumCount releases';
+ }
+
+ @override
+ String get discographyAlbumsOnly => 'Albums Only';
+
+ @override
+ String discographyAlbumsOnlySubtitle(int count, int albumCount) {
+ return '$count tracks from $albumCount albums';
+ }
+
+ @override
+ String get discographySinglesOnly => 'Singles & EPs Only';
+
+ @override
+ String discographySinglesOnlySubtitle(int count, int albumCount) {
+ return '$count tracks from $albumCount singles';
+ }
+
+ @override
+ String get discographySelectAlbums => 'Select Albums...';
+
+ @override
+ String get discographySelectAlbumsSubtitle =>
+ 'Choose specific albums or singles';
+
+ @override
+ String get discographyFetchingTracks => 'Fetching tracks...';
+
+ @override
+ String discographyFetchingAlbum(int current, int total) {
+ return 'Fetching $current of $total...';
+ }
+
+ @override
+ String discographySelectedCount(int count) {
+ return '$count selected';
+ }
+
+ @override
+ String get discographyDownloadSelected => 'Download Selected';
+
+ @override
+ String discographyAddedToQueue(int count) {
+ return 'Added $count tracks to queue';
+ }
+
+ @override
+ String discographySkippedDownloaded(int added, int skipped) {
+ return '$added added, $skipped already downloaded';
+ }
+
+ @override
+ String get discographyNoAlbums => 'No albums available';
+
+ @override
+ String get discographyFailedToFetch => 'Failed to fetch some albums';
+
+ @override
+ String get sectionStorageAccess => 'Storage Access';
+
+ @override
+ String get allFilesAccess => 'All Files Access';
+
+ @override
+ String get allFilesAccessEnabledSubtitle => 'Can write to any folder';
+
+ @override
+ String get allFilesAccessDisabledSubtitle => 'Limited to media folders only';
+
+ @override
+ String get allFilesAccessDescription =>
+ 'Enable this if you encounter write errors when saving to custom folders. Android 13+ restricts access to certain directories by default.';
+
+ @override
+ String get allFilesAccessDeniedMessage =>
+ 'Permission was denied. Please enable \'All files access\' manually in system settings.';
+
+ @override
+ String get allFilesAccessDisabledMessage =>
+ 'All Files Access disabled. The app will use limited storage access.';
+
+ @override
+ String get settingsLocalLibrary => 'Local Library';
+
+ @override
+ String get settingsLocalLibrarySubtitle => 'Scan music & detect duplicates';
+
+ @override
+ String get settingsCache => 'Storage & Cache';
+
+ @override
+ String get settingsCacheSubtitle => 'View size and clear cached data';
+
+ @override
+ String get libraryTitle => 'Local Library';
+
+ @override
+ String get libraryScanSettings => 'Scan Settings';
+
+ @override
+ String get libraryEnableLocalLibrary => 'Enable Local Library';
+
+ @override
+ String get libraryEnableLocalLibrarySubtitle =>
+ 'Scan and track your existing music';
+
+ @override
+ String get libraryFolder => 'Library Folder';
+
+ @override
+ String get libraryFolderHint => 'Tap to select folder';
+
+ @override
+ String get libraryShowDuplicateIndicator => 'Show Duplicate Indicator';
+
+ @override
+ String get libraryShowDuplicateIndicatorSubtitle =>
+ 'Show when searching for existing tracks';
+
+ @override
+ String get libraryActions => 'Actions';
+
+ @override
+ String get libraryScan => 'Scan Library';
+
+ @override
+ String get libraryScanSubtitle => 'Scan for audio files';
+
+ @override
+ String get libraryScanSelectFolderFirst => 'Select a folder first';
+
+ @override
+ String get libraryCleanupMissingFiles => 'Cleanup Missing Files';
+
+ @override
+ String get libraryCleanupMissingFilesSubtitle =>
+ 'Remove entries for files that no longer exist';
+
+ @override
+ String get libraryClear => 'Clear Library';
+
+ @override
+ String get libraryClearSubtitle => 'Remove all scanned tracks';
+
+ @override
+ String get libraryClearConfirmTitle => 'Clear Library';
+
+ @override
+ String get libraryClearConfirmMessage =>
+ 'This will remove all scanned tracks from your library. Your actual music files will not be deleted.';
+
+ @override
+ String get libraryAbout => 'About Local Library';
+
+ @override
+ String get libraryAboutDescription =>
+ 'Scans your existing music collection to detect duplicates when downloading. Supports FLAC, M4A, MP3, Opus, and OGG formats. Metadata is read from file tags when available.';
+
+ @override
+ String libraryTracksUnit(int count) {
+ String _temp0 = intl.Intl.pluralLogic(
+ count,
+ locale: localeName,
+ other: 'tracks',
+ one: 'track',
+ );
+ return '$_temp0';
+ }
+
+ @override
+ String libraryLastScanned(String time) {
+ return 'Last scanned: $time';
+ }
+
+ @override
+ String get libraryLastScannedNever => 'Never';
+
+ @override
+ String get libraryScanning => 'Scanning...';
+
+ @override
+ String libraryScanProgress(String progress, int total) {
+ return '$progress% of $total files';
+ }
+
+ @override
+ String get libraryInLibrary => 'In Library';
+
+ @override
+ String libraryRemovedMissingFiles(int count) {
+ return 'Removed $count missing files from library';
+ }
+
+ @override
+ String get libraryCleared => 'Library cleared';
+
+ @override
+ String get libraryStorageAccessRequired => 'Storage Access Required';
+
+ @override
+ String get libraryStorageAccessMessage =>
+ 'SpotiFLAC needs storage access to scan your music library. Please grant permission in settings.';
+
+ @override
+ String get libraryFolderNotExist => 'Selected folder does not exist';
+
+ @override
+ String get librarySourceDownloaded => 'Downloaded';
+
+ @override
+ String get librarySourceLocal => 'Local';
+
+ @override
+ String get libraryFilterAll => 'All';
+
+ @override
+ String get libraryFilterDownloaded => 'Downloaded';
+
+ @override
+ String get libraryFilterLocal => 'Local';
+
+ @override
+ String get libraryFilterTitle => 'Filters';
+
+ @override
+ String get libraryFilterReset => 'Reset';
+
+ @override
+ String get libraryFilterApply => 'Apply';
+
+ @override
+ String get libraryFilterSource => 'Source';
+
+ @override
+ String get libraryFilterQuality => 'Quality';
+
+ @override
+ String get libraryFilterQualityHiRes => 'Hi-Res (24bit)';
+
+ @override
+ String get libraryFilterQualityCD => 'CD (16bit)';
+
+ @override
+ String get libraryFilterQualityLossy => 'Lossy';
+
+ @override
+ String get libraryFilterFormat => 'Format';
+
+ @override
+ String get libraryFilterSort => 'Sort';
+
+ @override
+ String get libraryFilterSortLatest => 'Latest';
+
+ @override
+ String get libraryFilterSortOldest => 'Oldest';
+
+ @override
+ String get timeJustNow => 'Just now';
+
+ @override
+ String timeMinutesAgo(int count) {
+ String _temp0 = intl.Intl.pluralLogic(
+ count,
+ locale: localeName,
+ other: '$count minutes ago',
+ one: '1 minute ago',
+ );
+ return '$_temp0';
+ }
+
+ @override
+ String timeHoursAgo(int count) {
+ String _temp0 = intl.Intl.pluralLogic(
+ count,
+ locale: localeName,
+ other: '$count hours ago',
+ one: '1 hour ago',
+ );
+ return '$_temp0';
+ }
+
+ @override
+ String get tutorialWelcomeTitle => 'Welcome to SpotiFLAC!';
+
+ @override
+ String get tutorialWelcomeDesc =>
+ 'Let\'s learn how to download your favorite music in lossless quality. This quick tutorial will show you the basics.';
+
+ @override
+ String get tutorialWelcomeTip1 =>
+ 'Download music from Spotify, Deezer, or paste any supported URL';
+
+ @override
+ String get tutorialWelcomeTip2 =>
+ 'Get FLAC quality audio from Tidal, Qobuz, or Deezer';
+
+ @override
+ String get tutorialWelcomeTip3 =>
+ 'Automatic metadata, cover art, and lyrics embedding';
+
+ @override
+ String get tutorialSearchTitle => 'Finding Music';
+
+ @override
+ String get tutorialSearchDesc =>
+ 'There are two easy ways to find music you want to download.';
+
+ @override
+ String get tutorialDownloadTitle => 'Downloading Music';
+
+ @override
+ String get tutorialDownloadDesc =>
+ 'Downloading music is simple and fast. Here\'s how it works.';
+
+ @override
+ String get tutorialLibraryTitle => 'Your Library';
+
+ @override
+ String get tutorialLibraryDesc =>
+ 'All your downloaded music is organized in the Library tab.';
+
+ @override
+ String get tutorialLibraryTip1 =>
+ 'View download progress and queue in the Library tab';
+
+ @override
+ String get tutorialLibraryTip2 =>
+ 'Tap any track to play it with your music player';
+
+ @override
+ String get tutorialLibraryTip3 =>
+ 'Switch between list and grid view for better browsing';
+
+ @override
+ String get tutorialExtensionsTitle => 'Extensions';
+
+ @override
+ String get tutorialExtensionsDesc =>
+ 'Extend the app\'s capabilities with community extensions.';
+
+ @override
+ String get tutorialExtensionsTip1 =>
+ 'Browse the Store tab to discover useful extensions';
+
+ @override
+ String get tutorialExtensionsTip2 =>
+ 'Add new download providers or search sources';
+
+ @override
+ String get tutorialExtensionsTip3 =>
+ 'Get lyrics, enhanced metadata, and more features';
+
+ @override
+ String get tutorialSettingsTitle => 'Customize Your Experience';
+
+ @override
+ String get tutorialSettingsDesc =>
+ 'Personalize the app in Settings to match your preferences.';
+
+ @override
+ String get tutorialSettingsTip1 =>
+ 'Change download location and folder organization';
+
+ @override
+ String get tutorialSettingsTip2 =>
+ 'Set default audio quality and format preferences';
+
+ @override
+ String get tutorialSettingsTip3 => 'Customize app theme and appearance';
+
+ @override
+ String get tutorialReadyMessage =>
+ 'You\'re all set! Start downloading your favorite music now.';
+
+ @override
+ String get libraryForceFullScan => 'Force Full Scan';
+
+ @override
+ String get libraryForceFullScanSubtitle => 'Rescan all files, ignoring cache';
+
+ @override
+ String get cleanupOrphanedDownloads => 'Cleanup Orphaned Downloads';
+
+ @override
+ String get cleanupOrphanedDownloadsSubtitle =>
+ 'Remove history entries for files that no longer exist';
+
+ @override
+ String cleanupOrphanedDownloadsResult(int count) {
+ return 'Removed $count orphaned entries from history';
+ }
+
+ @override
+ String get cleanupOrphanedDownloadsNone => 'No orphaned entries found';
+
+ @override
+ String get cacheTitle => 'Storage & Cache';
+
+ @override
+ String get cacheSummaryTitle => 'Cache overview';
+
+ @override
+ String get cacheSummarySubtitle =>
+ 'Clearing cache will not remove downloaded music files.';
+
+ @override
+ String cacheEstimatedTotal(String size) {
+ return 'Estimated cache usage: $size';
+ }
+
+ @override
+ String get cacheSectionStorage => 'Cached Data';
+
+ @override
+ String get cacheSectionMaintenance => 'Maintenance';
+
+ @override
+ String get cacheAppDirectory => 'App cache directory';
+
+ @override
+ String get cacheAppDirectoryDesc =>
+ 'HTTP responses, WebView data, and other temporary app data.';
+
+ @override
+ String get cacheTempDirectory => 'Temporary directory';
+
+ @override
+ String get cacheTempDirectoryDesc =>
+ 'Temporary files from downloads and audio conversion.';
+
+ @override
+ String get cacheCoverImage => 'Cover image cache';
+
+ @override
+ String get cacheCoverImageDesc =>
+ 'Downloaded album and track cover art. Will re-download when viewed.';
+
+ @override
+ String get cacheLibraryCover => 'Library cover cache';
+
+ @override
+ String get cacheLibraryCoverDesc =>
+ 'Cover art extracted from local music files. Will re-extract on next scan.';
+
+ @override
+ String get cacheExploreFeed => 'Explore feed cache';
+
+ @override
+ String get cacheExploreFeedDesc =>
+ 'Explore tab content (new releases, trending). Will refresh on next visit.';
+
+ @override
+ String get cacheTrackLookup => 'Track lookup cache';
+
+ @override
+ String get cacheTrackLookupDesc =>
+ 'Spotify/Deezer track ID lookups. Clearing may slow next few searches.';
+
+ @override
+ String get cacheCleanupUnusedDesc =>
+ 'Remove orphaned download history and library entries for missing files.';
+
+ @override
+ String get cacheNoData => 'No cached data';
+
+ @override
+ String cacheSizeWithFiles(String size, int count) {
+ return '$size in $count files';
+ }
+
+ @override
+ String cacheSizeOnly(String size) {
+ return '$size';
+ }
+
+ @override
+ String cacheEntries(int count) {
+ return '$count entries';
+ }
+
+ @override
+ String cacheClearSuccess(String target) {
+ return 'Cleared: $target';
+ }
+
+ @override
+ String get cacheClearConfirmTitle => 'Clear cache?';
+
+ @override
+ String cacheClearConfirmMessage(String target) {
+ return 'This will clear cached data for $target. Downloaded music files will not be deleted.';
+ }
+
+ @override
+ String get cacheClearAllConfirmTitle => 'Clear all cache?';
+
+ @override
+ String get cacheClearAllConfirmMessage =>
+ 'This will clear all cache categories on this page. Downloaded music files will not be deleted.';
+
+ @override
+ String get cacheClearAll => 'Clear all cache';
+
+ @override
+ String get cacheCleanupUnused => 'Cleanup unused data';
+
+ @override
+ String get cacheCleanupUnusedSubtitle =>
+ 'Remove orphaned download history and missing library entries';
+
+ @override
+ String cacheCleanupResult(int downloadCount, int libraryCount) {
+ return 'Cleanup completed: $downloadCount orphaned downloads, $libraryCount missing library entries';
+ }
+
+ @override
+ String get cacheRefreshStats => 'Refresh stats';
+
+ @override
+ String get trackSaveCoverArt => 'Save Cover Art';
+
+ @override
+ String get trackSaveCoverArtSubtitle => 'Save album art as .jpg file';
+
+ @override
+ String get trackSaveLyrics => 'Save Lyrics (.lrc)';
+
+ @override
+ String get trackSaveLyricsSubtitle => 'Fetch and save lyrics as .lrc file';
+
+ @override
+ String get trackSaveLyricsProgress => 'Saving lyrics...';
+
+ @override
+ String get trackReEnrich => 'Re-enrich';
+
+ @override
+ String get trackReEnrichOnlineSubtitle =>
+ 'Search metadata online and embed into file';
+
+ @override
+ String get trackEditMetadata => 'Edit Metadata';
+
+ @override
+ String trackCoverSaved(String fileName) {
+ return 'Cover art saved to $fileName';
+ }
+
+ @override
+ String get trackCoverNoSource => 'No cover art source available';
+
+ @override
+ String trackLyricsSaved(String fileName) {
+ return 'Lyrics saved to $fileName';
+ }
+
+ @override
+ String get trackReEnrichProgress => 'Re-enriching metadata...';
+
+ @override
+ String get trackReEnrichSearching => 'Searching metadata online...';
+
+ @override
+ String get trackReEnrichSuccess => 'Metadata re-enriched successfully';
+
+ @override
+ String get trackReEnrichFfmpegFailed => 'FFmpeg metadata embed failed';
+
+ @override
+ String trackSaveFailed(String error) {
+ return 'Failed: $error';
+ }
+
+ @override
+ String get trackConvertFormat => 'Convert Format';
+
+ @override
+ String get trackConvertFormatSubtitle => 'Convert to MP3 or Opus';
+
+ @override
+ String get trackConvertTitle => 'Convert Audio';
+
+ @override
+ String get trackConvertTargetFormat => 'Target Format';
+
+ @override
+ String get trackConvertBitrate => 'Bitrate';
+
+ @override
+ String get trackConvertConfirmTitle => 'Confirm Conversion';
+
+ @override
+ String trackConvertConfirmMessage(
+ String sourceFormat,
+ String targetFormat,
+ String bitrate,
+ ) {
+ return 'Convert from $sourceFormat to $targetFormat at $bitrate?\n\nThe original file will be deleted after conversion.';
+ }
+
+ @override
+ String get trackConvertConverting => 'Converting audio...';
+
+ @override
+ String trackConvertSuccess(String format) {
+ return 'Converted to $format successfully';
+ }
+
+ @override
+ String get trackConvertFailed => 'Conversion failed';
+
+ @override
+ String get cueSplitTitle => 'Split CUE Sheet';
+
+ @override
+ String get cueSplitSubtitle => 'Split CUE+FLAC into individual tracks';
+
+ @override
+ String cueSplitAlbum(String album) {
+ return 'Album: $album';
+ }
+
+ @override
+ String cueSplitArtist(String artist) {
+ return 'Artist: $artist';
+ }
+
+ @override
+ String cueSplitTrackCount(int count) {
+ return '$count tracks';
+ }
+
+ @override
+ String get cueSplitConfirmTitle => 'Split CUE Album';
+
+ @override
+ String cueSplitConfirmMessage(String album, int count) {
+ return 'Split \"$album\" into $count individual FLAC files?\n\nFiles will be saved to the same directory.';
+ }
+
+ @override
+ String cueSplitSplitting(int current, int total) {
+ return 'Splitting CUE sheet... ($current/$total)';
+ }
+
+ @override
+ String cueSplitSuccess(int count) {
+ return 'Split into $count tracks successfully';
+ }
+
+ @override
+ String get cueSplitFailed => 'CUE split failed';
+
+ @override
+ String get cueSplitNoAudioFile => 'Audio file not found for this CUE sheet';
+
+ @override
+ String get cueSplitButton => 'Split into Tracks';
+
+ @override
+ String get actionCreate => 'Create';
+
+ @override
+ String get collectionFoldersTitle => 'My folders';
+
+ @override
+ String get collectionWishlist => 'Wishlist';
+
+ @override
+ String get collectionLoved => 'Loved';
+
+ @override
+ String get collectionPlaylists => 'Playlists';
+
+ @override
+ String get collectionPlaylist => 'Playlist';
+
+ @override
+ String get collectionAddToPlaylist => 'Add to playlist';
+
+ @override
+ String get collectionCreatePlaylist => 'Create playlist';
+
+ @override
+ String get collectionNoPlaylistsYet => 'No playlists yet';
+
+ @override
+ String get collectionNoPlaylistsSubtitle =>
+ 'Create a playlist to start categorizing tracks';
+
+ @override
+ String collectionPlaylistTracks(int count) {
+ String _temp0 = intl.Intl.pluralLogic(
+ count,
+ locale: localeName,
+ other: '$count tracks',
+ one: '1 track',
+ );
+ return '$_temp0';
+ }
+
+ @override
+ String collectionAddedToPlaylist(String playlistName) {
+ return 'Added to \"$playlistName\"';
+ }
+
+ @override
+ String collectionAlreadyInPlaylist(String playlistName) {
+ return 'Already in \"$playlistName\"';
+ }
+
+ @override
+ String get collectionPlaylistCreated => 'Playlist created';
+
+ @override
+ String get collectionPlaylistNameHint => 'Playlist name';
+
+ @override
+ String get collectionPlaylistNameRequired => 'Playlist name is required';
+
+ @override
+ String get collectionRenamePlaylist => 'Rename playlist';
+
+ @override
+ String get collectionDeletePlaylist => 'Delete playlist';
+
+ @override
+ String collectionDeletePlaylistMessage(String playlistName) {
+ return 'Delete \"$playlistName\" and all tracks inside it?';
+ }
+
+ @override
+ String get collectionPlaylistDeleted => 'Playlist deleted';
+
+ @override
+ String get collectionPlaylistRenamed => 'Playlist renamed';
+
+ @override
+ String get collectionWishlistEmptyTitle => 'Wishlist is empty';
+
+ @override
+ String get collectionWishlistEmptySubtitle =>
+ 'Tap + on tracks to save what you want to download later';
+
+ @override
+ String get collectionLovedEmptyTitle => 'Loved folder is empty';
+
+ @override
+ String get collectionLovedEmptySubtitle =>
+ 'Tap love on tracks to keep your favorites';
+
+ @override
+ String get collectionPlaylistEmptyTitle => 'Playlist is empty';
+
+ @override
+ String get collectionPlaylistEmptySubtitle =>
+ 'Long-press + on any track to add it here';
+
+ @override
+ String get collectionRemoveFromPlaylist => 'Remove from playlist';
+
+ @override
+ String get collectionRemoveFromFolder => 'Remove from folder';
+
+ @override
+ String collectionRemoved(String trackName) {
+ return '\"$trackName\" removed';
+ }
+
+ @override
+ String collectionAddedToLoved(String trackName) {
+ return '\"$trackName\" added to Loved';
+ }
+
+ @override
+ String collectionRemovedFromLoved(String trackName) {
+ return '\"$trackName\" removed from Loved';
+ }
+
+ @override
+ String collectionAddedToWishlist(String trackName) {
+ return '\"$trackName\" added to Wishlist';
+ }
+
+ @override
+ String collectionRemovedFromWishlist(String trackName) {
+ return '\"$trackName\" removed from Wishlist';
+ }
+
+ @override
+ String get trackOptionAddToLoved => 'Add to Loved';
+
+ @override
+ String get trackOptionRemoveFromLoved => 'Remove from Loved';
+
+ @override
+ String get trackOptionAddToWishlist => 'Add to Wishlist';
+
+ @override
+ String get trackOptionRemoveFromWishlist => 'Remove from Wishlist';
+
+ @override
+ String get collectionPlaylistChangeCover => 'Change cover image';
+
+ @override
+ String get collectionPlaylistRemoveCover => 'Remove cover image';
+
+ @override
+ String selectionShareCount(int count) {
+ String _temp0 = intl.Intl.pluralLogic(
+ count,
+ locale: localeName,
+ other: 'tracks',
+ one: 'track',
+ );
+ return 'Share $count $_temp0';
+ }
+
+ @override
+ String get selectionShareNoFiles => 'No shareable files found';
+
+ @override
+ String selectionConvertCount(int count) {
+ String _temp0 = intl.Intl.pluralLogic(
+ count,
+ locale: localeName,
+ other: 'tracks',
+ one: 'track',
+ );
+ return 'Convert $count $_temp0';
+ }
+
+ @override
+ String get selectionConvertNoConvertible => 'No convertible tracks selected';
+
+ @override
+ String get selectionBatchConvertConfirmTitle => 'Batch Convert';
+
+ @override
+ String selectionBatchConvertConfirmMessage(
+ int count,
+ String format,
+ String bitrate,
+ ) {
+ String _temp0 = intl.Intl.pluralLogic(
+ count,
+ locale: localeName,
+ other: 'tracks',
+ one: 'track',
+ );
+ return 'Convert $count $_temp0 to $format at $bitrate?\n\nOriginal files will be deleted after conversion.';
+ }
+
+ @override
+ String selectionBatchConvertProgress(int current, int total) {
+ return 'Converting $current of $total...';
+ }
+
+ @override
+ String selectionBatchConvertSuccess(int success, int total, String format) {
+ return 'Converted $success of $total tracks to $format';
+ }
+
+ @override
+ String downloadedAlbumDownloadedCount(int count) {
+ return '$count downloaded';
+ }
+
+ @override
+ String get downloadUseAlbumArtistForFoldersAlbumSubtitle =>
+ 'Artist folders use Album Artist when available';
+
+ @override
+ String get downloadUseAlbumArtistForFoldersTrackSubtitle =>
+ 'Artist folders use Track Artist only';
+}
+
+/// The translations for Chinese, as used in China (`zh_CN`).
+class AppLocalizationsZhCn extends AppLocalizationsZh {
+ AppLocalizationsZhCn() : super('zh_CN');
+
+ @override
+ String get appName => 'SpotiFLAC';
+
+ @override
+ String get navHome => 'Home';
+
+ @override
+ String get navLibrary => 'Library';
+
+ @override
+ String get navSettings => 'Settings';
+
+ @override
+ String get navStore => 'Store';
+
+ @override
+ String get homeTitle => 'Home';
+
+ @override
+ String get homeSubtitle => 'Paste a Spotify link or search by name';
+
+ @override
+ String get homeSupports => 'Supports: Track, Album, Playlist, Artist URLs';
+
+ @override
+ String get homeRecent => 'Recent';
+
+ @override
+ String get historyFilterAll => 'All';
+
+ @override
+ String get historyFilterAlbums => 'Albums';
+
+ @override
+ String get historyFilterSingles => 'Singles';
+
+ @override
+ String get historySearchHint => 'Search history...';
+
+ @override
+ String get settingsTitle => 'Settings';
+
+ @override
+ String get settingsDownload => 'Download';
+
+ @override
+ String get settingsAppearance => 'Appearance';
+
+ @override
+ String get settingsOptions => 'Options';
+
+ @override
+ String get settingsExtensions => 'Extensions';
+
+ @override
+ String get settingsAbout => 'About';
+
+ @override
+ String get downloadTitle => 'Download';
+
+ @override
+ String get downloadAskQualitySubtitle =>
+ 'Show quality picker for each download';
+
+ @override
+ String get downloadFilenameFormat => 'Filename Format';
+
+ @override
+ String get downloadFolderOrganization => 'Folder Organization';
+
+ @override
+ String get appearanceTitle => 'Appearance';
+
+ @override
+ String get appearanceThemeSystem => 'System';
+
+ @override
+ String get appearanceThemeLight => 'Light';
+
+ @override
+ String get appearanceThemeDark => 'Dark';
+
+ @override
+ String get appearanceDynamicColor => 'Dynamic Color';
+
+ @override
+ String get appearanceDynamicColorSubtitle => 'Use colors from your wallpaper';
+
+ @override
+ String get appearanceHistoryView => 'History View';
+
+ @override
+ String get appearanceHistoryViewList => 'List';
+
+ @override
+ String get appearanceHistoryViewGrid => 'Grid';
+
+ @override
+ String get optionsTitle => 'Options';
+
+ @override
+ String get optionsPrimaryProvider => 'Primary Provider';
+
+ @override
+ String get optionsPrimaryProviderSubtitle =>
+ 'Service used when searching by track name.';
+
+ @override
+ String optionsUsingExtension(String extensionName) {
+ return 'Using extension: $extensionName';
+ }
+
+ @override
+ String get optionsSwitchBack =>
+ 'Tap Deezer or Spotify to switch back from extension';
+
+ @override
+ String get optionsAutoFallback => 'Auto Fallback';
+
+ @override
+ String get optionsAutoFallbackSubtitle =>
+ 'Try other services if download fails';
+
+ @override
+ String get optionsUseExtensionProviders => 'Use Extension Providers';
+
+ @override
+ String get optionsUseExtensionProvidersOn => 'Extensions will be tried first';
+
+ @override
+ String get optionsUseExtensionProvidersOff => 'Using built-in providers only';
+
+ @override
+ String get optionsEmbedLyrics => 'Embed Lyrics';
+
+ @override
+ String get optionsEmbedLyricsSubtitle =>
+ 'Embed synced lyrics into FLAC files';
+
+ @override
+ String get optionsMaxQualityCover => 'Max Quality Cover';
+
+ @override
+ String get optionsMaxQualityCoverSubtitle =>
+ 'Download highest resolution cover art';
+
+ @override
+ String get optionsConcurrentDownloads => 'Concurrent Downloads';
+
+ @override
+ String get optionsConcurrentSequential => 'Sequential (1 at a time)';
+
+ @override
+ String optionsConcurrentParallel(int count) {
+ return '$count parallel downloads';
+ }
+
+ @override
+ String get optionsConcurrentWarning =>
+ 'Parallel downloads may trigger rate limiting';
+
+ @override
+ String get optionsExtensionStore => 'Extension Store';
+
+ @override
+ String get optionsExtensionStoreSubtitle => 'Show Store tab in navigation';
+
+ @override
+ String get optionsCheckUpdates => 'Check for Updates';
+
+ @override
+ String get optionsCheckUpdatesSubtitle =>
+ 'Notify when new version is available';
+
+ @override
+ String get optionsUpdateChannel => 'Update Channel';
+
+ @override
+ String get optionsUpdateChannelStable => 'Stable releases only';
+
+ @override
+ String get optionsUpdateChannelPreview => 'Get preview releases';
+
+ @override
+ String get optionsUpdateChannelWarning =>
+ 'Preview may contain bugs or incomplete features';
+
+ @override
+ String get optionsClearHistory => 'Clear Download History';
+
+ @override
+ String get optionsClearHistorySubtitle =>
+ 'Remove all downloaded tracks from history';
+
+ @override
+ String get optionsDetailedLogging => 'Detailed Logging';
+
+ @override
+ String get optionsDetailedLoggingOn => 'Detailed logs are being recorded';
+
+ @override
+ String get optionsDetailedLoggingOff => 'Enable for bug reports';
+
+ @override
+ String get optionsSpotifyCredentials => 'Spotify Credentials';
+
+ @override
+ String optionsSpotifyCredentialsConfigured(String clientId) {
+ return 'Client ID: $clientId...';
+ }
+
+ @override
+ String get optionsSpotifyCredentialsRequired => 'Required - tap to configure';
+
+ @override
+ String get optionsSpotifyWarning =>
+ 'Spotify requires your own API credentials. Get them free from developer.spotify.com';
+
+ @override
+ String get optionsSpotifyDeprecationWarning =>
+ 'Spotify search will be deprecated on March 3, 2026 due to Spotify API changes. Please switch to Deezer.';
+
+ @override
+ String get extensionsTitle => 'Extensions';
+
+ @override
+ String get extensionsDisabled => 'Disabled';
+
+ @override
+ String extensionsVersion(String version) {
+ return 'Version $version';
+ }
+
+ @override
+ String extensionsAuthor(String author) {
+ return 'by $author';
+ }
+
+ @override
+ String get extensionsUninstall => 'Uninstall';
+
+ @override
+ String get storeTitle => 'Extension Store';
+
+ @override
+ String get storeSearch => 'Search extensions...';
+
+ @override
+ String get storeInstall => 'Install';
+
+ @override
+ String get storeInstalled => 'Installed';
+
+ @override
+ String get storeUpdate => 'Update';
+
+ @override
+ String get aboutTitle => 'About';
+
+ @override
+ String get aboutContributors => 'Contributors';
+
+ @override
+ String get aboutMobileDeveloper => 'Mobile version developer';
+
+ @override
+ String get aboutOriginalCreator => 'Creator of the original SpotiFLAC';
+
+ @override
+ String get aboutLogoArtist =>
+ 'The talented artist who created our beautiful app logo!';
+
+ @override
+ String get aboutTranslators => 'Translators';
+
+ @override
+ String get aboutSpecialThanks => 'Special Thanks';
+
+ @override
+ String get aboutLinks => 'Links';
+
+ @override
+ String get aboutMobileSource => 'Mobile source code';
+
+ @override
+ String get aboutPCSource => 'PC source code';
+
+ @override
+ String get aboutReportIssue => 'Report an issue';
+
+ @override
+ String get aboutReportIssueSubtitle => 'Report any problems you encounter';
+
+ @override
+ String get aboutFeatureRequest => 'Feature request';
+
+ @override
+ String get aboutFeatureRequestSubtitle => 'Suggest new features for the app';
+
+ @override
+ String get aboutTelegramChannel => 'Telegram Channel';
+
+ @override
+ String get aboutTelegramChannelSubtitle => 'Announcements and updates';
+
+ @override
+ String get aboutTelegramChat => 'Telegram Community';
+
+ @override
+ String get aboutTelegramChatSubtitle => 'Chat with other users';
+
+ @override
+ String get aboutSocial => 'Social';
+
+ @override
+ String get aboutApp => 'App';
+
+ @override
+ String get aboutVersion => 'Version';
+
+ @override
+ String get aboutBinimumDesc =>
+ 'The creator of QQDL & HiFi API. Without this API, Tidal downloads wouldn\'t exist!';
+
+ @override
+ String get aboutSachinsenalDesc =>
+ 'The original HiFi project creator. The foundation of Tidal integration!';
+
+ @override
+ String get aboutSjdonadoDesc =>
+ 'Creator of I Don\'t Have Spotify (IDHS). The fallback link resolver that saves the day!';
+
+ @override
+ String get aboutDabMusic => 'DAB Music';
+
+ @override
+ String get aboutDabMusicDesc =>
+ 'The best Qobuz streaming API. Hi-Res downloads wouldn\'t be possible without this!';
+
+ @override
+ String get aboutSpotiSaver => 'SpotiSaver';
+
@override
String get aboutSpotiSaverDesc =>
'Tidal Hi-Res FLAC streaming endpoints. A key piece of the lossless puzzle!';
@@ -2344,2118 +4750,6 @@ class AppLocalizationsZh extends AppLocalizations {
'Artist folders use Track Artist only';
}
-/// The translations for Chinese, as used in China (`zh_CN`).
-class AppLocalizationsZhCn extends AppLocalizationsZh {
- AppLocalizationsZhCn() : super('zh_CN');
-
- @override
- String get appName => 'SpotiFLAC';
-
- @override
- String get navHome => 'Home';
-
- @override
- String get navLibrary => 'Library';
-
- @override
- String get navSettings => 'Settings';
-
- @override
- String get navStore => 'Store';
-
- @override
- String get homeTitle => 'Home';
-
- @override
- String get homeSubtitle => 'Paste a Spotify link or search by name';
-
- @override
- String get homeSupports => 'Supports: Track, Album, Playlist, Artist URLs';
-
- @override
- String get homeRecent => 'Recent';
-
- @override
- String get historyFilterAll => 'All';
-
- @override
- String get historyFilterAlbums => 'Albums';
-
- @override
- String get historyFilterSingles => 'Singles';
-
- @override
- String get historySearchHint => 'Search history...';
-
- @override
- String get settingsTitle => 'Settings';
-
- @override
- String get settingsDownload => 'Download';
-
- @override
- String get settingsAppearance => 'Appearance';
-
- @override
- String get settingsOptions => 'Options';
-
- @override
- String get settingsExtensions => 'Extensions';
-
- @override
- String get settingsAbout => 'About';
-
- @override
- String get downloadTitle => 'Download';
-
- @override
- String get downloadAskQualitySubtitle =>
- 'Show quality picker for each download';
-
- @override
- String get downloadFilenameFormat => 'Filename Format';
-
- @override
- String get downloadFolderOrganization => 'Folder Organization';
-
- @override
- String get appearanceTitle => 'Appearance';
-
- @override
- String get appearanceThemeSystem => 'System';
-
- @override
- String get appearanceThemeLight => 'Light';
-
- @override
- String get appearanceThemeDark => 'Dark';
-
- @override
- String get appearanceDynamicColor => 'Dynamic Color';
-
- @override
- String get appearanceDynamicColorSubtitle => 'Use colors from your wallpaper';
-
- @override
- String get appearanceHistoryView => 'History View';
-
- @override
- String get appearanceHistoryViewList => 'List';
-
- @override
- String get appearanceHistoryViewGrid => 'Grid';
-
- @override
- String get optionsTitle => 'Options';
-
- @override
- String get optionsPrimaryProvider => 'Primary Provider';
-
- @override
- String get optionsPrimaryProviderSubtitle =>
- 'Service used when searching by track name.';
-
- @override
- String optionsUsingExtension(String extensionName) {
- return 'Using extension: $extensionName';
- }
-
- @override
- String get optionsSwitchBack =>
- 'Tap Deezer or Spotify to switch back from extension';
-
- @override
- String get optionsAutoFallback => 'Auto Fallback';
-
- @override
- String get optionsAutoFallbackSubtitle =>
- 'Try other services if download fails';
-
- @override
- String get optionsUseExtensionProviders => 'Use Extension Providers';
-
- @override
- String get optionsUseExtensionProvidersOn => 'Extensions will be tried first';
-
- @override
- String get optionsUseExtensionProvidersOff => 'Using built-in providers only';
-
- @override
- String get optionsEmbedLyrics => 'Embed Lyrics';
-
- @override
- String get optionsEmbedLyricsSubtitle =>
- 'Embed synced lyrics into FLAC files';
-
- @override
- String get optionsMaxQualityCover => 'Max Quality Cover';
-
- @override
- String get optionsMaxQualityCoverSubtitle =>
- 'Download highest resolution cover art';
-
- @override
- String get optionsConcurrentDownloads => 'Concurrent Downloads';
-
- @override
- String get optionsConcurrentSequential => 'Sequential (1 at a time)';
-
- @override
- String optionsConcurrentParallel(int count) {
- return '$count parallel downloads';
- }
-
- @override
- String get optionsConcurrentWarning =>
- 'Parallel downloads may trigger rate limiting';
-
- @override
- String get optionsExtensionStore => 'Extension Store';
-
- @override
- String get optionsExtensionStoreSubtitle => 'Show Store tab in navigation';
-
- @override
- String get optionsCheckUpdates => 'Check for Updates';
-
- @override
- String get optionsCheckUpdatesSubtitle =>
- 'Notify when new version is available';
-
- @override
- String get optionsUpdateChannel => 'Update Channel';
-
- @override
- String get optionsUpdateChannelStable => 'Stable releases only';
-
- @override
- String get optionsUpdateChannelPreview => 'Get preview releases';
-
- @override
- String get optionsUpdateChannelWarning =>
- 'Preview may contain bugs or incomplete features';
-
- @override
- String get optionsClearHistory => 'Clear Download History';
-
- @override
- String get optionsClearHistorySubtitle =>
- 'Remove all downloaded tracks from history';
-
- @override
- String get optionsDetailedLogging => 'Detailed Logging';
-
- @override
- String get optionsDetailedLoggingOn => 'Detailed logs are being recorded';
-
- @override
- String get optionsDetailedLoggingOff => 'Enable for bug reports';
-
- @override
- String get optionsSpotifyCredentials => 'Spotify Credentials';
-
- @override
- String optionsSpotifyCredentialsConfigured(String clientId) {
- return 'Client ID: $clientId...';
- }
-
- @override
- String get optionsSpotifyCredentialsRequired => 'Required - tap to configure';
-
- @override
- String get optionsSpotifyWarning =>
- 'Spotify requires your own API credentials. Get them free from developer.spotify.com';
-
- @override
- String get optionsSpotifyDeprecationWarning =>
- 'Spotify search will be deprecated on March 3, 2026 due to Spotify API changes. Please switch to Deezer.';
-
- @override
- String get extensionsTitle => 'Extensions';
-
- @override
- String get extensionsDisabled => 'Disabled';
-
- @override
- String extensionsVersion(String version) {
- return 'Version $version';
- }
-
- @override
- String extensionsAuthor(String author) {
- return 'by $author';
- }
-
- @override
- String get extensionsUninstall => 'Uninstall';
-
- @override
- String get storeTitle => 'Extension Store';
-
- @override
- String get storeSearch => 'Search extensions...';
-
- @override
- String get storeInstall => 'Install';
-
- @override
- String get storeInstalled => 'Installed';
-
- @override
- String get storeUpdate => 'Update';
-
- @override
- String get aboutTitle => 'About';
-
- @override
- String get aboutContributors => 'Contributors';
-
- @override
- String get aboutMobileDeveloper => 'Mobile version developer';
-
- @override
- String get aboutOriginalCreator => 'Creator of the original SpotiFLAC';
-
- @override
- String get aboutLogoArtist =>
- 'The talented artist who created our beautiful app logo!';
-
- @override
- String get aboutTranslators => 'Translators';
-
- @override
- String get aboutSpecialThanks => 'Special Thanks';
-
- @override
- String get aboutLinks => 'Links';
-
- @override
- String get aboutMobileSource => 'Mobile source code';
-
- @override
- String get aboutPCSource => 'PC source code';
-
- @override
- String get aboutReportIssue => 'Report an issue';
-
- @override
- String get aboutReportIssueSubtitle => 'Report any problems you encounter';
-
- @override
- String get aboutFeatureRequest => 'Feature request';
-
- @override
- String get aboutFeatureRequestSubtitle => 'Suggest new features for the app';
-
- @override
- String get aboutTelegramChannel => 'Telegram Channel';
-
- @override
- String get aboutTelegramChannelSubtitle => 'Announcements and updates';
-
- @override
- String get aboutTelegramChat => 'Telegram Community';
-
- @override
- String get aboutTelegramChatSubtitle => 'Chat with other users';
-
- @override
- String get aboutSocial => 'Social';
-
- @override
- String get aboutApp => 'App';
-
- @override
- String get aboutVersion => 'Version';
-
- @override
- String get aboutBinimumDesc =>
- 'The creator of QQDL & HiFi API. Without this API, Tidal downloads wouldn\'t exist!';
-
- @override
- String get aboutSachinsenalDesc =>
- 'The original HiFi project creator. The foundation of Tidal integration!';
-
- @override
- String get aboutSjdonadoDesc =>
- 'Creator of I Don\'t Have Spotify (IDHS). The fallback link resolver that saves the day!';
-
- @override
- String get aboutDabMusic => 'DAB Music';
-
- @override
- String get aboutDabMusicDesc =>
- 'The best Qobuz streaming API. Hi-Res downloads wouldn\'t be possible without this!';
-
- @override
- String get aboutSpotiSaver => 'SpotiSaver';
-
- @override
- String get aboutSpotiSaverDesc =>
- 'Tidal Hi-Res FLAC streaming endpoints. A key piece of the lossless puzzle!';
-
- @override
- String get aboutAppDescription =>
- 'Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.';
-
- @override
- String get artistAlbums => 'Albums';
-
- @override
- String get artistSingles => 'Singles & EPs';
-
- @override
- String get artistCompilations => 'Compilations';
-
- @override
- String get artistPopular => 'Popular';
-
- @override
- String artistMonthlyListeners(String count) {
- return '$count monthly listeners';
- }
-
- @override
- String get trackMetadataService => 'Service';
-
- @override
- String get trackMetadataPlay => 'Play';
-
- @override
- String get trackMetadataShare => 'Share';
-
- @override
- String get trackMetadataDelete => 'Delete';
-
- @override
- String get setupGrantPermission => 'Grant Permission';
-
- @override
- String get setupSkip => 'Skip for now';
-
- @override
- String get setupStorageAccessRequired => 'Storage Access Required';
-
- @override
- String get setupStorageAccessMessageAndroid11 =>
- 'Android 11+ requires \"All files access\" permission to save files to your chosen download folder.';
-
- @override
- String get setupOpenSettings => 'Open Settings';
-
- @override
- String get setupPermissionDeniedMessage =>
- 'Permission denied. Please grant all permissions to continue.';
-
- @override
- String setupPermissionRequired(String permissionType) {
- return '$permissionType Permission Required';
- }
-
- @override
- String setupPermissionRequiredMessage(String permissionType) {
- return '$permissionType permission is required for the best experience. You can change this later in Settings.';
- }
-
- @override
- String get setupUseDefaultFolder => 'Use Default Folder?';
-
- @override
- String get setupNoFolderSelected =>
- 'No folder selected. Would you like to use the default Music folder?';
-
- @override
- String get setupUseDefault => 'Use Default';
-
- @override
- String get setupDownloadLocationTitle => 'Download Location';
-
- @override
- String get setupDownloadLocationIosMessage =>
- 'On iOS, downloads are saved to the app\'s Documents folder. You can access them via the Files app.';
-
- @override
- String get setupAppDocumentsFolder => 'App Documents Folder';
-
- @override
- String get setupAppDocumentsFolderSubtitle =>
- 'Recommended - accessible via Files app';
-
- @override
- String get setupChooseFromFiles => 'Choose from Files';
-
- @override
- String get setupChooseFromFilesSubtitle => 'Select iCloud or other location';
-
- @override
- String get setupIosEmptyFolderWarning =>
- 'iOS limitation: Empty folders cannot be selected. Choose a folder with at least one file.';
-
- @override
- String get setupIcloudNotSupported =>
- 'iCloud Drive is not supported. Please use the app Documents folder.';
-
- @override
- String get setupDownloadInFlac => 'Download Spotify tracks in FLAC';
-
- @override
- String get setupStorageGranted => 'Storage Permission Granted!';
-
- @override
- String get setupStorageRequired => 'Storage Permission Required';
-
- @override
- String get setupStorageDescription =>
- 'SpotiFLAC needs storage permission to save your downloaded music files.';
-
- @override
- String get setupNotificationGranted => 'Notification Permission Granted!';
-
- @override
- String get setupNotificationEnable => 'Enable Notifications';
-
- @override
- String get setupFolderChoose => 'Choose Download Folder';
-
- @override
- String get setupFolderDescription =>
- 'Select a folder where your downloaded music will be saved.';
-
- @override
- String get setupSelectFolder => 'Select Folder';
-
- @override
- String get setupEnableNotifications => 'Enable Notifications';
-
- @override
- String get setupNotificationBackgroundDescription =>
- 'Get notified about download progress and completion. This helps you track downloads when the app is in background.';
-
- @override
- String get setupSkipForNow => 'Skip for now';
-
- @override
- String get setupNext => 'Next';
-
- @override
- String get setupGetStarted => 'Get Started';
-
- @override
- String get setupAllowAccessToManageFiles =>
- 'Please enable \"Allow access to manage all files\" in the next screen.';
-
- @override
- String get dialogCancel => 'Cancel';
-
- @override
- String get dialogSave => 'Save';
-
- @override
- String get dialogDelete => 'Delete';
-
- @override
- String get dialogRetry => 'Retry';
-
- @override
- String get dialogClear => 'Clear';
-
- @override
- String get dialogDone => 'Done';
-
- @override
- String get dialogImport => 'Import';
-
- @override
- String get dialogDiscard => 'Discard';
-
- @override
- String get dialogRemove => 'Remove';
-
- @override
- String get dialogUninstall => 'Uninstall';
-
- @override
- String get dialogDiscardChanges => 'Discard Changes?';
-
- @override
- String get dialogUnsavedChanges =>
- 'You have unsaved changes. Do you want to discard them?';
-
- @override
- String get dialogClearAll => 'Clear All';
-
- @override
- String get dialogRemoveExtension => 'Remove Extension';
-
- @override
- String get dialogRemoveExtensionMessage =>
- 'Are you sure you want to remove this extension? This cannot be undone.';
-
- @override
- String get dialogUninstallExtension => 'Uninstall Extension?';
-
- @override
- String dialogUninstallExtensionMessage(String extensionName) {
- return 'Are you sure you want to remove $extensionName?';
- }
-
- @override
- String get dialogClearHistoryTitle => 'Clear History';
-
- @override
- String get dialogClearHistoryMessage =>
- 'Are you sure you want to clear all download history? This cannot be undone.';
-
- @override
- String get dialogDeleteSelectedTitle => 'Delete Selected';
-
- @override
- String dialogDeleteSelectedMessage(int count) {
- String _temp0 = intl.Intl.pluralLogic(
- count,
- locale: localeName,
- other: 'tracks',
- one: 'track',
- );
- return 'Delete $count $_temp0 from history?\n\nThis will also delete the files from storage.';
- }
-
- @override
- String get dialogImportPlaylistTitle => 'Import Playlist';
-
- @override
- String dialogImportPlaylistMessage(int count) {
- return 'Found $count tracks in CSV. Add them to download queue?';
- }
-
- @override
- String csvImportTracks(int count) {
- return '$count tracks from CSV';
- }
-
- @override
- String snackbarAddedToQueue(String trackName) {
- return 'Added \"$trackName\" to queue';
- }
-
- @override
- String snackbarAddedTracksToQueue(int count) {
- return 'Added $count tracks to queue';
- }
-
- @override
- String snackbarAlreadyDownloaded(String trackName) {
- return '\"$trackName\" already downloaded';
- }
-
- @override
- String snackbarAlreadyInLibrary(String trackName) {
- return '\"$trackName\" already exists in your library';
- }
-
- @override
- String get snackbarHistoryCleared => 'History cleared';
-
- @override
- String get snackbarCredentialsSaved => 'Credentials saved';
-
- @override
- String get snackbarCredentialsCleared => 'Credentials cleared';
-
- @override
- String snackbarDeletedTracks(int count) {
- String _temp0 = intl.Intl.pluralLogic(
- count,
- locale: localeName,
- other: 'tracks',
- one: 'track',
- );
- return 'Deleted $count $_temp0';
- }
-
- @override
- String snackbarCannotOpenFile(String error) {
- return 'Cannot open file: $error';
- }
-
- @override
- String get snackbarFillAllFields => 'Please fill all fields';
-
- @override
- String get snackbarViewQueue => 'View Queue';
-
- @override
- String snackbarUrlCopied(String platform) {
- return '$platform URL copied to clipboard';
- }
-
- @override
- String get snackbarFileNotFound => 'File not found';
-
- @override
- String get snackbarSelectExtFile => 'Please select a .spotiflac-ext file';
-
- @override
- String get snackbarProviderPrioritySaved => 'Provider priority saved';
-
- @override
- String get snackbarMetadataProviderSaved =>
- 'Metadata provider priority saved';
-
- @override
- String snackbarExtensionInstalled(String extensionName) {
- return '$extensionName installed.';
- }
-
- @override
- String snackbarExtensionUpdated(String extensionName) {
- return '$extensionName updated.';
- }
-
- @override
- String get snackbarFailedToInstall => 'Failed to install extension';
-
- @override
- String get snackbarFailedToUpdate => 'Failed to update extension';
-
- @override
- String get errorRateLimited => 'Rate Limited';
-
- @override
- String get errorRateLimitedMessage =>
- 'Too many requests. Please wait a moment before searching again.';
-
- @override
- String get errorNoTracksFound => 'No tracks found';
-
- @override
- String errorMissingExtensionSource(String item) {
- return 'Cannot load $item: missing extension source';
- }
-
- @override
- String get actionPause => 'Pause';
-
- @override
- String get actionResume => 'Resume';
-
- @override
- String get actionCancel => 'Cancel';
-
- @override
- String get actionSelectAll => 'Select All';
-
- @override
- String get actionDeselect => 'Deselect';
-
- @override
- String get actionRemoveCredentials => 'Remove Credentials';
-
- @override
- String get actionSaveCredentials => 'Save Credentials';
-
- @override
- String selectionSelected(int count) {
- return '$count selected';
- }
-
- @override
- String get selectionAllSelected => 'All tracks selected';
-
- @override
- String get selectionSelectToDelete => 'Select tracks to delete';
-
- @override
- String progressFetchingMetadata(int current, int total) {
- return 'Fetching metadata... $current/$total';
- }
-
- @override
- String get progressReadingCsv => 'Reading CSV...';
-
- @override
- String get searchSongs => 'Songs';
-
- @override
- String get searchArtists => 'Artists';
-
- @override
- String get searchAlbums => 'Albums';
-
- @override
- String get searchPlaylists => 'Playlists';
-
- @override
- String get tooltipPlay => 'Play';
-
- @override
- String get filenameFormat => 'Filename Format';
-
- @override
- String get folderOrganizationNone => 'No organization';
-
- @override
- String get folderOrganizationByArtist => 'By Artist';
-
- @override
- String get folderOrganizationByAlbum => 'By Album';
-
- @override
- String get folderOrganizationByArtistAlbum => 'Artist/Album';
-
- @override
- String get folderOrganizationDescription =>
- 'Organize downloaded files into folders';
-
- @override
- String get folderOrganizationNoneSubtitle => 'All files in download folder';
-
- @override
- String get folderOrganizationByArtistSubtitle =>
- 'Separate folder for each artist';
-
- @override
- String get folderOrganizationByAlbumSubtitle =>
- 'Separate folder for each album';
-
- @override
- String get folderOrganizationByArtistAlbumSubtitle =>
- 'Nested folders for artist and album';
-
- @override
- String get updateAvailable => 'Update Available';
-
- @override
- String get updateLater => 'Later';
-
- @override
- String get updateStartingDownload => 'Starting download...';
-
- @override
- String get updateDownloadFailed => 'Download failed';
-
- @override
- String get updateFailedMessage => 'Failed to download update';
-
- @override
- String get updateNewVersionReady => 'A new version is ready';
-
- @override
- String get updateCurrent => 'Current';
-
- @override
- String get updateNew => 'New';
-
- @override
- String get updateDownloading => 'Downloading...';
-
- @override
- String get updateWhatsNew => 'What\'s New';
-
- @override
- String get updateDownloadInstall => 'Download & Install';
-
- @override
- String get updateDontRemind => 'Don\'t remind';
-
- @override
- String get providerPriorityTitle => 'Provider Priority';
-
- @override
- String get providerPriorityDescription =>
- 'Drag to reorder download providers. The app will try providers from top to bottom when downloading tracks.';
-
- @override
- String get providerPriorityInfo =>
- 'If a track is not available on the first provider, the app will automatically try the next one.';
-
- @override
- String get providerBuiltIn => 'Built-in';
-
- @override
- String get providerExtension => 'Extension';
-
- @override
- String get metadataProviderPriorityTitle => 'Metadata Priority';
-
- @override
- String get metadataProviderPriorityDescription =>
- 'Drag to reorder metadata providers. The app will try providers from top to bottom when searching for tracks and fetching metadata.';
-
- @override
- String get metadataProviderPriorityInfo =>
- 'Deezer has no rate limits and is recommended as primary. Spotify may rate limit after many requests.';
-
- @override
- String get metadataNoRateLimits => 'No rate limits';
-
- @override
- String get metadataMayRateLimit => 'May rate limit';
-
- @override
- String get logTitle => 'Logs';
-
- @override
- String get logCopied => 'Logs copied to clipboard';
-
- @override
- String get logSearchHint => 'Search logs...';
-
- @override
- String get logFilterLevel => 'Level';
-
- @override
- String get logFilterSection => 'Filter';
-
- @override
- String get logShareLogs => 'Share logs';
-
- @override
- String get logClearLogs => 'Clear logs';
-
- @override
- String get logClearLogsTitle => 'Clear Logs';
-
- @override
- String get logClearLogsMessage => 'Are you sure you want to clear all logs?';
-
- @override
- String get logFilterBySeverity => 'Filter logs by severity';
-
- @override
- String get logNoLogsYet => 'No logs yet';
-
- @override
- String get logNoLogsYetSubtitle => 'Logs will appear here as you use the app';
-
- @override
- String logEntriesFiltered(int count) {
- return 'Entries ($count filtered)';
- }
-
- @override
- String logEntries(int count) {
- return 'Entries ($count)';
- }
-
- @override
- String get credentialsTitle => 'Spotify Credentials';
-
- @override
- String get credentialsDescription =>
- 'Enter your Client ID and Secret to use your own Spotify application quota.';
-
- @override
- String get credentialsClientId => 'Client ID';
-
- @override
- String get credentialsClientIdHint => 'Paste Client ID';
-
- @override
- String get credentialsClientSecret => 'Client Secret';
-
- @override
- String get credentialsClientSecretHint => 'Paste Client Secret';
-
- @override
- String get channelStable => 'Stable';
-
- @override
- String get channelPreview => 'Preview';
-
- @override
- String get sectionSearchSource => 'Search Source';
-
- @override
- String get sectionDownload => 'Download';
-
- @override
- String get sectionPerformance => 'Performance';
-
- @override
- String get sectionApp => 'App';
-
- @override
- String get sectionData => 'Data';
-
- @override
- String get sectionDebug => 'Debug';
-
- @override
- String get sectionService => 'Service';
-
- @override
- String get sectionAudioQuality => 'Audio Quality';
-
- @override
- String get sectionFileSettings => 'File Settings';
-
- @override
- String get sectionLyrics => 'Lyrics';
-
- @override
- String get lyricsMode => 'Lyrics Mode';
-
- @override
- String get lyricsModeDescription =>
- 'Choose how lyrics are saved with your downloads';
-
- @override
- String get lyricsModeEmbed => 'Embed in file';
-
- @override
- String get lyricsModeEmbedSubtitle => 'Lyrics stored inside FLAC metadata';
-
- @override
- String get lyricsModeExternal => 'External .lrc file';
-
- @override
- String get lyricsModeExternalSubtitle =>
- 'Separate .lrc file for players like Samsung Music';
-
- @override
- String get lyricsModeBoth => 'Both';
-
- @override
- String get lyricsModeBothSubtitle => 'Embed and save .lrc file';
-
- @override
- String get sectionColor => 'Color';
-
- @override
- String get sectionTheme => 'Theme';
-
- @override
- String get sectionLayout => 'Layout';
-
- @override
- String get sectionLanguage => 'Language';
-
- @override
- String get appearanceLanguage => 'App Language';
-
- @override
- String get settingsAppearanceSubtitle => 'Theme, colors, display';
-
- @override
- String get settingsDownloadSubtitle => 'Service, quality, filename format';
-
- @override
- String get settingsOptionsSubtitle => 'Fallback, lyrics, cover art, updates';
-
- @override
- String get settingsExtensionsSubtitle => 'Manage download providers';
-
- @override
- String get settingsLogsSubtitle => 'View app logs for debugging';
-
- @override
- String get loadingSharedLink => 'Loading shared link...';
-
- @override
- String get pressBackAgainToExit => 'Press back again to exit';
-
- @override
- String downloadAllCount(int count) {
- return 'Download All ($count)';
- }
-
- @override
- String tracksCount(int count) {
- String _temp0 = intl.Intl.pluralLogic(
- count,
- locale: localeName,
- other: '$count tracks',
- one: '1 track',
- );
- return '$_temp0';
- }
-
- @override
- String get trackCopyFilePath => 'Copy file path';
-
- @override
- String get trackRemoveFromDevice => 'Remove from device';
-
- @override
- String get trackLoadLyrics => 'Load Lyrics';
-
- @override
- String get trackMetadata => 'Metadata';
-
- @override
- String get trackFileInfo => 'File Info';
-
- @override
- String get trackLyrics => 'Lyrics';
-
- @override
- String get trackFileNotFound => 'File not found';
-
- @override
- String get trackOpenInDeezer => 'Open in Deezer';
-
- @override
- String get trackOpenInSpotify => 'Open in Spotify';
-
- @override
- String get trackTrackName => 'Track name';
-
- @override
- String get trackArtist => 'Artist';
-
- @override
- String get trackAlbumArtist => 'Album artist';
-
- @override
- String get trackAlbum => 'Album';
-
- @override
- String get trackTrackNumber => 'Track number';
-
- @override
- String get trackDiscNumber => 'Disc number';
-
- @override
- String get trackDuration => 'Duration';
-
- @override
- String get trackAudioQuality => 'Audio quality';
-
- @override
- String get trackReleaseDate => 'Release date';
-
- @override
- String get trackGenre => 'Genre';
-
- @override
- String get trackLabel => 'Label';
-
- @override
- String get trackCopyright => 'Copyright';
-
- @override
- String get trackDownloaded => 'Downloaded';
-
- @override
- String get trackCopyLyrics => 'Copy lyrics';
-
- @override
- String get trackLyricsNotAvailable => 'Lyrics not available for this track';
-
- @override
- String get trackLyricsTimeout => 'Request timed out. Try again later.';
-
- @override
- String get trackLyricsLoadFailed => 'Failed to load lyrics';
-
- @override
- String get trackEmbedLyrics => 'Embed Lyrics';
-
- @override
- String get trackLyricsEmbedded => 'Lyrics embedded successfully';
-
- @override
- String get trackInstrumental => 'Instrumental track';
-
- @override
- String get trackCopiedToClipboard => 'Copied to clipboard';
-
- @override
- String get trackDeleteConfirmTitle => 'Remove from device?';
-
- @override
- String get trackDeleteConfirmMessage =>
- 'This will permanently delete the downloaded file and remove it from your history.';
-
- @override
- String get dateToday => 'Today';
-
- @override
- String get dateYesterday => 'Yesterday';
-
- @override
- String dateDaysAgo(int count) {
- return '$count days ago';
- }
-
- @override
- String dateWeeksAgo(int count) {
- return '$count weeks ago';
- }
-
- @override
- String dateMonthsAgo(int count) {
- return '$count months ago';
- }
-
- @override
- String get storeFilterAll => 'All';
-
- @override
- String get storeFilterMetadata => 'Metadata';
-
- @override
- String get storeFilterDownload => 'Download';
-
- @override
- String get storeFilterUtility => 'Utility';
-
- @override
- String get storeFilterLyrics => 'Lyrics';
-
- @override
- String get storeFilterIntegration => 'Integration';
-
- @override
- String get storeClearFilters => 'Clear filters';
-
- @override
- String get extensionDefaultProvider => 'Default (Deezer/Spotify)';
-
- @override
- String get extensionDefaultProviderSubtitle => 'Use built-in search';
-
- @override
- String get extensionAuthor => 'Author';
-
- @override
- String get extensionId => 'ID';
-
- @override
- String get extensionError => 'Error';
-
- @override
- String get extensionCapabilities => 'Capabilities';
-
- @override
- String get extensionMetadataProvider => 'Metadata Provider';
-
- @override
- String get extensionDownloadProvider => 'Download Provider';
-
- @override
- String get extensionLyricsProvider => 'Lyrics Provider';
-
- @override
- String get extensionUrlHandler => 'URL Handler';
-
- @override
- String get extensionQualityOptions => 'Quality Options';
-
- @override
- String get extensionPostProcessingHooks => 'Post-Processing Hooks';
-
- @override
- String get extensionPermissions => 'Permissions';
-
- @override
- String get extensionSettings => 'Settings';
-
- @override
- String get extensionRemoveButton => 'Remove Extension';
-
- @override
- String get extensionUpdated => 'Updated';
-
- @override
- String get extensionMinAppVersion => 'Min App Version';
-
- @override
- String get extensionCustomTrackMatching => 'Custom Track Matching';
-
- @override
- String get extensionPostProcessing => 'Post-Processing';
-
- @override
- String extensionHooksAvailable(int count) {
- return '$count hook(s) available';
- }
-
- @override
- String extensionPatternsCount(int count) {
- return '$count pattern(s)';
- }
-
- @override
- String extensionStrategy(String strategy) {
- return 'Strategy: $strategy';
- }
-
- @override
- String get extensionsProviderPrioritySection => 'Provider Priority';
-
- @override
- String get extensionsInstalledSection => 'Installed Extensions';
-
- @override
- String get extensionsNoExtensions => 'No extensions installed';
-
- @override
- String get extensionsNoExtensionsSubtitle =>
- 'Install .spotiflac-ext files to add new providers';
-
- @override
- String get extensionsInstallButton => 'Install Extension';
-
- @override
- String get extensionsInfoTip =>
- 'Extensions can add new metadata and download providers. Only install extensions from trusted sources.';
-
- @override
- String get extensionsInstalledSuccess => 'Extension installed successfully';
-
- @override
- String get extensionsDownloadPriority => 'Download Priority';
-
- @override
- String get extensionsDownloadPrioritySubtitle => 'Set download service order';
-
- @override
- String get extensionsNoDownloadProvider =>
- 'No extensions with download provider';
-
- @override
- String get extensionsMetadataPriority => 'Metadata Priority';
-
- @override
- String get extensionsMetadataPrioritySubtitle =>
- 'Set search & metadata source order';
-
- @override
- String get extensionsNoMetadataProvider =>
- 'No extensions with metadata provider';
-
- @override
- String get extensionsSearchProvider => 'Search Provider';
-
- @override
- String get extensionsNoCustomSearch => 'No extensions with custom search';
-
- @override
- String get extensionsSearchProviderDescription =>
- 'Choose which service to use for searching tracks';
-
- @override
- String get extensionsCustomSearch => 'Custom search';
-
- @override
- String get extensionsErrorLoading => 'Error loading extension';
-
- @override
- String get qualityFlacLossless => 'FLAC Lossless';
-
- @override
- String get qualityFlacLosslessSubtitle => '16-bit / 44.1kHz';
-
- @override
- String get qualityHiResFlac => 'Hi-Res FLAC';
-
- @override
- String get qualityHiResFlacSubtitle => '24-bit / up to 96kHz';
-
- @override
- String get qualityHiResFlacMax => 'Hi-Res FLAC Max';
-
- @override
- String get qualityHiResFlacMaxSubtitle => '24-bit / up to 192kHz';
-
- @override
- String get qualityNote =>
- 'Actual quality depends on track availability from the service';
-
- @override
- String get youtubeQualityNote =>
- 'YouTube provides lossy audio only. Not part of lossless fallback.';
-
- @override
- String get downloadAskBeforeDownload => 'Ask Before Download';
-
- @override
- String get downloadDirectory => 'Download Directory';
-
- @override
- String get downloadSeparateSinglesFolder => 'Separate Singles Folder';
-
- @override
- String get downloadAlbumFolderStructure => 'Album Folder Structure';
-
- @override
- String get downloadUseAlbumArtistForFolders => 'Use Album Artist for folders';
-
- @override
- String get downloadUsePrimaryArtistOnly => 'Primary artist only for folders';
-
- @override
- String get downloadUsePrimaryArtistOnlyEnabled =>
- 'Featured artists removed from folder name (e.g. Justin Bieber, Quavo → Justin Bieber)';
-
- @override
- String get downloadUsePrimaryArtistOnlyDisabled =>
- 'Full artist string used for folder name';
-
- @override
- String get downloadSelectQuality => 'Select Quality';
-
- @override
- String get downloadFrom => 'Download From';
-
- @override
- String get appearanceAmoledDark => 'AMOLED Dark';
-
- @override
- String get appearanceAmoledDarkSubtitle => 'Pure black background';
-
- @override
- String get queueClearAll => 'Clear All';
-
- @override
- String get queueClearAllMessage =>
- 'Are you sure you want to clear all downloads?';
-
- @override
- String get settingsAutoExportFailed => 'Auto-export failed downloads';
-
- @override
- String get settingsAutoExportFailedSubtitle =>
- 'Save failed downloads to TXT file automatically';
-
- @override
- String get settingsDownloadNetwork => 'Download Network';
-
- @override
- String get settingsDownloadNetworkAny => 'WiFi + Mobile Data';
-
- @override
- String get settingsDownloadNetworkWifiOnly => 'WiFi Only';
-
- @override
- String get settingsDownloadNetworkSubtitle =>
- 'Choose which network to use for downloads. When set to WiFi Only, downloads will pause on mobile data.';
-
- @override
- String get albumFolderArtistAlbum => 'Artist / Album';
-
- @override
- String get albumFolderArtistAlbumSubtitle => 'Albums/Artist Name/Album Name/';
-
- @override
- String get albumFolderArtistYearAlbum => 'Artist / [Year] Album';
-
- @override
- String get albumFolderArtistYearAlbumSubtitle =>
- 'Albums/Artist Name/[2005] Album Name/';
-
- @override
- String get albumFolderAlbumOnly => 'Album Only';
-
- @override
- String get albumFolderAlbumOnlySubtitle => 'Albums/Album Name/';
-
- @override
- String get albumFolderYearAlbum => '[Year] Album';
-
- @override
- String get albumFolderYearAlbumSubtitle => 'Albums/[2005] Album Name/';
-
- @override
- String get albumFolderArtistAlbumSingles => 'Artist / Album + Singles';
-
- @override
- String get albumFolderArtistAlbumSinglesSubtitle =>
- 'Artist/Album/ and Artist/Singles/';
-
- @override
- String get downloadedAlbumDeleteSelected => 'Delete Selected';
-
- @override
- String downloadedAlbumDeleteMessage(int count) {
- String _temp0 = intl.Intl.pluralLogic(
- count,
- locale: localeName,
- other: 'tracks',
- one: 'track',
- );
- return 'Delete $count $_temp0 from this album?\n\nThis will also delete the files from storage.';
- }
-
- @override
- String downloadedAlbumSelectedCount(int count) {
- return '$count selected';
- }
-
- @override
- String get downloadedAlbumAllSelected => 'All tracks selected';
-
- @override
- String get downloadedAlbumTapToSelect => 'Tap tracks to select';
-
- @override
- String downloadedAlbumDeleteCount(int count) {
- String _temp0 = intl.Intl.pluralLogic(
- count,
- locale: localeName,
- other: 'tracks',
- one: 'track',
- );
- return 'Delete $count $_temp0';
- }
-
- @override
- String get downloadedAlbumSelectToDelete => 'Select tracks to delete';
-
- @override
- String downloadedAlbumDiscHeader(int discNumber) {
- return 'Disc $discNumber';
- }
-
- @override
- String get recentTypeArtist => 'Artist';
-
- @override
- String get recentTypeAlbum => 'Album';
-
- @override
- String get recentTypeSong => 'Song';
-
- @override
- String get recentTypePlaylist => 'Playlist';
-
- @override
- String get recentEmpty => 'No recent items yet';
-
- @override
- String get recentShowAllDownloads => 'Show All Downloads';
-
- @override
- String recentPlaylistInfo(String name) {
- return 'Playlist: $name';
- }
-
- @override
- String get discographyDownload => 'Download Discography';
-
- @override
- String get discographyDownloadAll => 'Download All';
-
- @override
- String discographyDownloadAllSubtitle(int count, int albumCount) {
- return '$count tracks from $albumCount releases';
- }
-
- @override
- String get discographyAlbumsOnly => 'Albums Only';
-
- @override
- String discographyAlbumsOnlySubtitle(int count, int albumCount) {
- return '$count tracks from $albumCount albums';
- }
-
- @override
- String get discographySinglesOnly => 'Singles & EPs Only';
-
- @override
- String discographySinglesOnlySubtitle(int count, int albumCount) {
- return '$count tracks from $albumCount singles';
- }
-
- @override
- String get discographySelectAlbums => 'Select Albums...';
-
- @override
- String get discographySelectAlbumsSubtitle =>
- 'Choose specific albums or singles';
-
- @override
- String get discographyFetchingTracks => 'Fetching tracks...';
-
- @override
- String discographyFetchingAlbum(int current, int total) {
- return 'Fetching $current of $total...';
- }
-
- @override
- String discographySelectedCount(int count) {
- return '$count selected';
- }
-
- @override
- String get discographyDownloadSelected => 'Download Selected';
-
- @override
- String discographyAddedToQueue(int count) {
- return 'Added $count tracks to queue';
- }
-
- @override
- String discographySkippedDownloaded(int added, int skipped) {
- return '$added added, $skipped already downloaded';
- }
-
- @override
- String get discographyNoAlbums => 'No albums available';
-
- @override
- String get discographyFailedToFetch => 'Failed to fetch some albums';
-
- @override
- String get sectionStorageAccess => 'Storage Access';
-
- @override
- String get allFilesAccess => 'All Files Access';
-
- @override
- String get allFilesAccessEnabledSubtitle => 'Can write to any folder';
-
- @override
- String get allFilesAccessDisabledSubtitle => 'Limited to media folders only';
-
- @override
- String get allFilesAccessDescription =>
- 'Enable this if you encounter write errors when saving to custom folders. Android 13+ restricts access to certain directories by default.';
-
- @override
- String get allFilesAccessDeniedMessage =>
- 'Permission was denied. Please enable \'All files access\' manually in system settings.';
-
- @override
- String get allFilesAccessDisabledMessage =>
- 'All Files Access disabled. The app will use limited storage access.';
-
- @override
- String get settingsLocalLibrary => 'Local Library';
-
- @override
- String get settingsLocalLibrarySubtitle => 'Scan music & detect duplicates';
-
- @override
- String get settingsCache => 'Storage & Cache';
-
- @override
- String get settingsCacheSubtitle => 'View size and clear cached data';
-
- @override
- String get libraryTitle => 'Local Library';
-
- @override
- String get libraryScanSettings => 'Scan Settings';
-
- @override
- String get libraryEnableLocalLibrary => 'Enable Local Library';
-
- @override
- String get libraryEnableLocalLibrarySubtitle =>
- 'Scan and track your existing music';
-
- @override
- String get libraryFolder => 'Library Folder';
-
- @override
- String get libraryFolderHint => 'Tap to select folder';
-
- @override
- String get libraryShowDuplicateIndicator => 'Show Duplicate Indicator';
-
- @override
- String get libraryShowDuplicateIndicatorSubtitle =>
- 'Show when searching for existing tracks';
-
- @override
- String get libraryActions => 'Actions';
-
- @override
- String get libraryScan => 'Scan Library';
-
- @override
- String get libraryScanSubtitle => 'Scan for audio files';
-
- @override
- String get libraryScanSelectFolderFirst => 'Select a folder first';
-
- @override
- String get libraryCleanupMissingFiles => 'Cleanup Missing Files';
-
- @override
- String get libraryCleanupMissingFilesSubtitle =>
- 'Remove entries for files that no longer exist';
-
- @override
- String get libraryClear => 'Clear Library';
-
- @override
- String get libraryClearSubtitle => 'Remove all scanned tracks';
-
- @override
- String get libraryClearConfirmTitle => 'Clear Library';
-
- @override
- String get libraryClearConfirmMessage =>
- 'This will remove all scanned tracks from your library. Your actual music files will not be deleted.';
-
- @override
- String get libraryAbout => 'About Local Library';
-
- @override
- String get libraryAboutDescription =>
- 'Scans your existing music collection to detect duplicates when downloading. Supports FLAC, M4A, MP3, Opus, and OGG formats. Metadata is read from file tags when available.';
-
- @override
- String libraryLastScanned(String time) {
- return 'Last scanned: $time';
- }
-
- @override
- String get libraryLastScannedNever => 'Never';
-
- @override
- String get libraryScanning => 'Scanning...';
-
- @override
- String libraryScanProgress(String progress, int total) {
- return '$progress% of $total files';
- }
-
- @override
- String get libraryInLibrary => 'In Library';
-
- @override
- String libraryRemovedMissingFiles(int count) {
- return 'Removed $count missing files from library';
- }
-
- @override
- String get libraryCleared => 'Library cleared';
-
- @override
- String get libraryStorageAccessRequired => 'Storage Access Required';
-
- @override
- String get libraryStorageAccessMessage =>
- 'SpotiFLAC needs storage access to scan your music library. Please grant permission in settings.';
-
- @override
- String get libraryFolderNotExist => 'Selected folder does not exist';
-
- @override
- String get librarySourceDownloaded => 'Downloaded';
-
- @override
- String get librarySourceLocal => 'Local';
-
- @override
- String get libraryFilterAll => 'All';
-
- @override
- String get libraryFilterDownloaded => 'Downloaded';
-
- @override
- String get libraryFilterLocal => 'Local';
-
- @override
- String get libraryFilterTitle => 'Filters';
-
- @override
- String get libraryFilterReset => 'Reset';
-
- @override
- String get libraryFilterApply => 'Apply';
-
- @override
- String get libraryFilterSource => 'Source';
-
- @override
- String get libraryFilterQuality => 'Quality';
-
- @override
- String get libraryFilterQualityHiRes => 'Hi-Res (24bit)';
-
- @override
- String get libraryFilterQualityCD => 'CD (16bit)';
-
- @override
- String get libraryFilterQualityLossy => 'Lossy';
-
- @override
- String get libraryFilterFormat => 'Format';
-
- @override
- String get libraryFilterSort => 'Sort';
-
- @override
- String get libraryFilterSortLatest => 'Latest';
-
- @override
- String get libraryFilterSortOldest => 'Oldest';
-
- @override
- String get timeJustNow => 'Just now';
-
- @override
- String timeMinutesAgo(int count) {
- String _temp0 = intl.Intl.pluralLogic(
- count,
- locale: localeName,
- other: '$count minutes ago',
- one: '1 minute ago',
- );
- return '$_temp0';
- }
-
- @override
- String timeHoursAgo(int count) {
- String _temp0 = intl.Intl.pluralLogic(
- count,
- locale: localeName,
- other: '$count hours ago',
- one: '1 hour ago',
- );
- return '$_temp0';
- }
-
- @override
- String get tutorialWelcomeTitle => 'Welcome to SpotiFLAC!';
-
- @override
- String get tutorialWelcomeDesc =>
- 'Let\'s learn how to download your favorite music in lossless quality. This quick tutorial will show you the basics.';
-
- @override
- String get tutorialWelcomeTip1 =>
- 'Download music from Spotify, Deezer, or paste any supported URL';
-
- @override
- String get tutorialWelcomeTip2 =>
- 'Get FLAC quality audio from Tidal, Qobuz, or Amazon Music';
-
- @override
- String get tutorialWelcomeTip3 =>
- 'Automatic metadata, cover art, and lyrics embedding';
-
- @override
- String get tutorialSearchTitle => 'Finding Music';
-
- @override
- String get tutorialSearchDesc =>
- 'There are two easy ways to find music you want to download.';
-
- @override
- String get tutorialDownloadTitle => 'Downloading Music';
-
- @override
- String get tutorialDownloadDesc =>
- 'Downloading music is simple and fast. Here\'s how it works.';
-
- @override
- String get tutorialLibraryTitle => 'Your Library';
-
- @override
- String get tutorialLibraryDesc =>
- 'All your downloaded music is organized in the Library tab.';
-
- @override
- String get tutorialLibraryTip1 =>
- 'View download progress and queue in the Library tab';
-
- @override
- String get tutorialLibraryTip2 =>
- 'Tap any track to play it with your music player';
-
- @override
- String get tutorialLibraryTip3 =>
- 'Switch between list and grid view for better browsing';
-
- @override
- String get tutorialExtensionsTitle => 'Extensions';
-
- @override
- String get tutorialExtensionsDesc =>
- 'Extend the app\'s capabilities with community extensions.';
-
- @override
- String get tutorialExtensionsTip1 =>
- 'Browse the Store tab to discover useful extensions';
-
- @override
- String get tutorialExtensionsTip2 =>
- 'Add new download providers or search sources';
-
- @override
- String get tutorialExtensionsTip3 =>
- 'Get lyrics, enhanced metadata, and more features';
-
- @override
- String get tutorialSettingsTitle => 'Customize Your Experience';
-
- @override
- String get tutorialSettingsDesc =>
- 'Personalize the app in Settings to match your preferences.';
-
- @override
- String get tutorialSettingsTip1 =>
- 'Change download location and folder organization';
-
- @override
- String get tutorialSettingsTip2 =>
- 'Set default audio quality and format preferences';
-
- @override
- String get tutorialSettingsTip3 => 'Customize app theme and appearance';
-
- @override
- String get tutorialReadyMessage =>
- 'You\'re all set! Start downloading your favorite music now.';
-
- @override
- String get libraryForceFullScan => 'Force Full Scan';
-
- @override
- String get libraryForceFullScanSubtitle => 'Rescan all files, ignoring cache';
-
- @override
- String get cleanupOrphanedDownloads => 'Cleanup Orphaned Downloads';
-
- @override
- String get cleanupOrphanedDownloadsSubtitle =>
- 'Remove history entries for files that no longer exist';
-
- @override
- String cleanupOrphanedDownloadsResult(int count) {
- return 'Removed $count orphaned entries from history';
- }
-
- @override
- String get cleanupOrphanedDownloadsNone => 'No orphaned entries found';
-
- @override
- String get cacheTitle => 'Storage & Cache';
-
- @override
- String get cacheSummaryTitle => 'Cache overview';
-
- @override
- String get cacheSummarySubtitle =>
- 'Clearing cache will not remove downloaded music files.';
-
- @override
- String cacheEstimatedTotal(String size) {
- return 'Estimated cache usage: $size';
- }
-
- @override
- String get cacheSectionStorage => 'Cached Data';
-
- @override
- String get cacheSectionMaintenance => 'Maintenance';
-
- @override
- String get cacheAppDirectory => 'App cache directory';
-
- @override
- String get cacheAppDirectoryDesc =>
- 'HTTP responses, WebView data, and other temporary app data.';
-
- @override
- String get cacheTempDirectory => 'Temporary directory';
-
- @override
- String get cacheTempDirectoryDesc =>
- 'Temporary files from downloads and audio conversion.';
-
- @override
- String get cacheCoverImage => 'Cover image cache';
-
- @override
- String get cacheCoverImageDesc =>
- 'Downloaded album and track cover art. Will re-download when viewed.';
-
- @override
- String get cacheLibraryCover => 'Library cover cache';
-
- @override
- String get cacheLibraryCoverDesc =>
- 'Cover art extracted from local music files. Will re-extract on next scan.';
-
- @override
- String get cacheExploreFeed => 'Explore feed cache';
-
- @override
- String get cacheExploreFeedDesc =>
- 'Explore tab content (new releases, trending). Will refresh on next visit.';
-
- @override
- String get cacheTrackLookup => 'Track lookup cache';
-
- @override
- String get cacheTrackLookupDesc =>
- 'Spotify/Deezer track ID lookups. Clearing may slow next few searches.';
-
- @override
- String get cacheCleanupUnusedDesc =>
- 'Remove orphaned download history and library entries for missing files.';
-
- @override
- String get cacheNoData => 'No cached data';
-
- @override
- String cacheSizeWithFiles(String size, int count) {
- return '$size in $count files';
- }
-
- @override
- String cacheSizeOnly(String size) {
- return '$size';
- }
-
- @override
- String cacheEntries(int count) {
- return '$count entries';
- }
-
- @override
- String cacheClearSuccess(String target) {
- return 'Cleared: $target';
- }
-
- @override
- String get cacheClearConfirmTitle => 'Clear cache?';
-
- @override
- String cacheClearConfirmMessage(String target) {
- return 'This will clear cached data for $target. Downloaded music files will not be deleted.';
- }
-
- @override
- String get cacheClearAllConfirmTitle => 'Clear all cache?';
-
- @override
- String get cacheClearAllConfirmMessage =>
- 'This will clear all cache categories on this page. Downloaded music files will not be deleted.';
-
- @override
- String get cacheClearAll => 'Clear all cache';
-
- @override
- String get cacheCleanupUnused => 'Cleanup unused data';
-
- @override
- String get cacheCleanupUnusedSubtitle =>
- 'Remove orphaned download history and missing library entries';
-
- @override
- String cacheCleanupResult(int downloadCount, int libraryCount) {
- return 'Cleanup completed: $downloadCount orphaned downloads, $libraryCount missing library entries';
- }
-
- @override
- String get cacheRefreshStats => 'Refresh stats';
-
- @override
- String get trackSaveCoverArt => 'Save Cover Art';
-
- @override
- String get trackSaveCoverArtSubtitle => 'Save album art as .jpg file';
-
- @override
- String get trackSaveLyrics => 'Save Lyrics (.lrc)';
-
- @override
- String get trackSaveLyricsSubtitle => 'Fetch and save lyrics as .lrc file';
-
- @override
- String get trackSaveLyricsProgress => 'Saving lyrics...';
-
- @override
- String get trackReEnrich => 'Re-enrich';
-
- @override
- String get trackReEnrichOnlineSubtitle =>
- 'Search metadata online and embed into file';
-
- @override
- String get trackEditMetadata => 'Edit Metadata';
-
- @override
- String trackCoverSaved(String fileName) {
- return 'Cover art saved to $fileName';
- }
-
- @override
- String get trackCoverNoSource => 'No cover art source available';
-
- @override
- String trackLyricsSaved(String fileName) {
- return 'Lyrics saved to $fileName';
- }
-
- @override
- String get trackReEnrichProgress => 'Re-enriching metadata...';
-
- @override
- String get trackReEnrichSearching => 'Searching metadata online...';
-
- @override
- String get trackReEnrichSuccess => 'Metadata re-enriched successfully';
-
- @override
- String get trackReEnrichFfmpegFailed => 'FFmpeg metadata embed failed';
-
- @override
- String trackSaveFailed(String error) {
- return 'Failed: $error';
- }
-
- @override
- String get trackConvertFormat => 'Convert Format';
-
- @override
- String get trackConvertFormatSubtitle => 'Convert to MP3 or Opus';
-
- @override
- String get trackConvertTitle => 'Convert Audio';
-
- @override
- String get trackConvertTargetFormat => 'Target Format';
-
- @override
- String get trackConvertBitrate => 'Bitrate';
-
- @override
- String get trackConvertConfirmTitle => 'Confirm Conversion';
-
- @override
- String trackConvertConfirmMessage(
- String sourceFormat,
- String targetFormat,
- String bitrate,
- ) {
- return 'Convert from $sourceFormat to $targetFormat at $bitrate?\n\nThe original file will be deleted after conversion.';
- }
-
- @override
- String get trackConvertConverting => 'Converting audio...';
-
- @override
- String trackConvertSuccess(String format) {
- return 'Converted to $format successfully';
- }
-
- @override
- String get trackConvertFailed => 'Conversion failed';
-
- @override
- String downloadedAlbumDownloadedCount(int count) {
- return '$count downloaded';
- }
-
- @override
- String get downloadUseAlbumArtistForFoldersAlbumSubtitle =>
- 'Artist folders use Album Artist when available';
-
- @override
- String get downloadUseAlbumArtistForFoldersTrackSubtitle =>
- 'Artist folders use Track Artist only';
-}
-
/// The translations for Chinese, as used in Taiwan (`zh_TW`).
class AppLocalizationsZhTw extends AppLocalizationsZh {
AppLocalizationsZhTw() : super('zh_TW');
@@ -5203,6 +5497,13 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
@override
String get filenameFormat => 'Filename Format';
+ @override
+ String get filenameShowAdvancedTags => 'Show advanced tags';
+
+ @override
+ String get filenameShowAdvancedTagsDescription =>
+ 'Enable formatted tags for track padding and date patterns';
+
@override
String get folderOrganizationNone => 'No organization';
@@ -5780,6 +6081,12 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
String get youtubeQualityNote =>
'YouTube provides lossy audio only. Not part of lossless fallback.';
+ @override
+ String get youtubeOpusBitrateTitle => 'YouTube Opus Bitrate';
+
+ @override
+ String get youtubeMp3BitrateTitle => 'YouTube MP3 Bitrate';
+
@override
String get downloadAskBeforeDownload => 'Ask Before Download';
@@ -6111,6 +6418,17 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
String get libraryAboutDescription =>
'Scans your existing music collection to detect duplicates when downloading. Supports FLAC, M4A, MP3, Opus, and OGG formats. Metadata is read from file tags when available.';
+ @override
+ String libraryTracksUnit(int count) {
+ String _temp0 = intl.Intl.pluralLogic(
+ count,
+ locale: localeName,
+ other: 'tracks',
+ one: 'track',
+ );
+ return '$_temp0';
+ }
+
@override
String libraryLastScanned(String time) {
return 'Last scanned: $time';
@@ -6554,6 +6872,210 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
@override
String get trackConvertFailed => 'Conversion failed';
+ @override
+ String get actionCreate => 'Create';
+
+ @override
+ String get collectionFoldersTitle => 'My folders';
+
+ @override
+ String get collectionWishlist => 'Wishlist';
+
+ @override
+ String get collectionLoved => 'Loved';
+
+ @override
+ String get collectionPlaylists => 'Playlists';
+
+ @override
+ String get collectionPlaylist => 'Playlist';
+
+ @override
+ String get collectionAddToPlaylist => 'Add to playlist';
+
+ @override
+ String get collectionCreatePlaylist => 'Create playlist';
+
+ @override
+ String get collectionNoPlaylistsYet => 'No playlists yet';
+
+ @override
+ String get collectionNoPlaylistsSubtitle =>
+ 'Create a playlist to start categorizing tracks';
+
+ @override
+ String collectionPlaylistTracks(int count) {
+ String _temp0 = intl.Intl.pluralLogic(
+ count,
+ locale: localeName,
+ other: '$count tracks',
+ one: '1 track',
+ );
+ return '$_temp0';
+ }
+
+ @override
+ String collectionAddedToPlaylist(String playlistName) {
+ return 'Added to \"$playlistName\"';
+ }
+
+ @override
+ String collectionAlreadyInPlaylist(String playlistName) {
+ return 'Already in \"$playlistName\"';
+ }
+
+ @override
+ String get collectionPlaylistCreated => 'Playlist created';
+
+ @override
+ String get collectionPlaylistNameHint => 'Playlist name';
+
+ @override
+ String get collectionPlaylistNameRequired => 'Playlist name is required';
+
+ @override
+ String get collectionRenamePlaylist => 'Rename playlist';
+
+ @override
+ String get collectionDeletePlaylist => 'Delete playlist';
+
+ @override
+ String collectionDeletePlaylistMessage(String playlistName) {
+ return 'Delete \"$playlistName\" and all tracks inside it?';
+ }
+
+ @override
+ String get collectionPlaylistDeleted => 'Playlist deleted';
+
+ @override
+ String get collectionPlaylistRenamed => 'Playlist renamed';
+
+ @override
+ String get collectionWishlistEmptyTitle => 'Wishlist is empty';
+
+ @override
+ String get collectionWishlistEmptySubtitle =>
+ 'Tap + on tracks to save what you want to download later';
+
+ @override
+ String get collectionLovedEmptyTitle => 'Loved folder is empty';
+
+ @override
+ String get collectionLovedEmptySubtitle =>
+ 'Tap love on tracks to keep your favorites';
+
+ @override
+ String get collectionPlaylistEmptyTitle => 'Playlist is empty';
+
+ @override
+ String get collectionPlaylistEmptySubtitle =>
+ 'Long-press + on any track to add it here';
+
+ @override
+ String get collectionRemoveFromPlaylist => 'Remove from playlist';
+
+ @override
+ String get collectionRemoveFromFolder => 'Remove from folder';
+
+ @override
+ String collectionRemoved(String trackName) {
+ return '\"$trackName\" removed';
+ }
+
+ @override
+ String collectionAddedToLoved(String trackName) {
+ return '\"$trackName\" added to Loved';
+ }
+
+ @override
+ String collectionRemovedFromLoved(String trackName) {
+ return '\"$trackName\" removed from Loved';
+ }
+
+ @override
+ String collectionAddedToWishlist(String trackName) {
+ return '\"$trackName\" added to Wishlist';
+ }
+
+ @override
+ String collectionRemovedFromWishlist(String trackName) {
+ return '\"$trackName\" removed from Wishlist';
+ }
+
+ @override
+ String get trackOptionAddToLoved => 'Add to Loved';
+
+ @override
+ String get trackOptionRemoveFromLoved => 'Remove from Loved';
+
+ @override
+ String get trackOptionAddToWishlist => 'Add to Wishlist';
+
+ @override
+ String get trackOptionRemoveFromWishlist => 'Remove from Wishlist';
+
+ @override
+ String get collectionPlaylistChangeCover => 'Change cover image';
+
+ @override
+ String get collectionPlaylistRemoveCover => 'Remove cover image';
+
+ @override
+ String selectionShareCount(int count) {
+ String _temp0 = intl.Intl.pluralLogic(
+ count,
+ locale: localeName,
+ other: 'tracks',
+ one: 'track',
+ );
+ return 'Share $count $_temp0';
+ }
+
+ @override
+ String get selectionShareNoFiles => 'No shareable files found';
+
+ @override
+ String selectionConvertCount(int count) {
+ String _temp0 = intl.Intl.pluralLogic(
+ count,
+ locale: localeName,
+ other: 'tracks',
+ one: 'track',
+ );
+ return 'Convert $count $_temp0';
+ }
+
+ @override
+ String get selectionConvertNoConvertible => 'No convertible tracks selected';
+
+ @override
+ String get selectionBatchConvertConfirmTitle => 'Batch Convert';
+
+ @override
+ String selectionBatchConvertConfirmMessage(
+ int count,
+ String format,
+ String bitrate,
+ ) {
+ String _temp0 = intl.Intl.pluralLogic(
+ count,
+ locale: localeName,
+ other: 'tracks',
+ one: 'track',
+ );
+ return 'Convert $count $_temp0 to $format at $bitrate?\n\nOriginal files will be deleted after conversion.';
+ }
+
+ @override
+ String selectionBatchConvertProgress(int current, int total) {
+ return 'Converting $current of $total...';
+ }
+
+ @override
+ String selectionBatchConvertSuccess(int success, int total, String format) {
+ return 'Converted $success of $total tracks to $format';
+ }
+
@override
String downloadedAlbumDownloadedCount(int count) {
return '$count downloaded';
diff --git a/lib/l10n/arb/app_de.arb b/lib/l10n/arb/app_de.arb
index b40c1f71..e32aa56e 100644
--- a/lib/l10n/arb/app_de.arb
+++ b/lib/l10n/arb/app_de.arb
@@ -9,7 +9,7 @@
"@navHome": {
"description": "Bottom navigation - Home tab"
},
- "navLibrary": "Archiv",
+ "navLibrary": "Bibliothek",
"@navLibrary": {
"description": "Bottom navigation - Library tab"
},
@@ -623,7 +623,7 @@
"@setupEnableNotifications": {
"description": "Button to enable notifications"
},
- "setupNotificationBackgroundDescription": "Werde benachrichtigt über Download-Fortschritt und -Fertigstellung. Dies hilft Ihnen, Downloads zu verfolgen, wenn die App im Hintergrund ist.",
+ "setupNotificationBackgroundDescription": "Erhalte Benachrichtigungen über den Fortschritt und die Fertigstellung deiner Downloads, selbst wenn die App im Hintergrund läuft.",
"@setupNotificationBackgroundDescription": {
"description": "Detailed notification explanation"
},
@@ -737,11 +737,11 @@
}
}
},
- "dialogImportPlaylistTitle": "Wiedergabeliste importieren",
+ "dialogImportPlaylistTitle": "Playlist importieren",
"@dialogImportPlaylistTitle": {
"description": "Dialog title - import CSV playlist"
},
- "dialogImportPlaylistMessage": "{count} Titel in CSV gefunden. Zur Warteschlange hinzufügen?",
+ "dialogImportPlaylistMessage": "{count} Titel gefunden hinzufügen?",
"csvImportTracks": "{count} Titel aus CSV",
"@csvImportTracks": {
"description": "Label shown in quality picker for CSV import",
@@ -759,7 +759,7 @@
}
}
},
- "snackbarAddedToQueue": "\"{trackName}\" zur Warteschlange hinzugefügt",
+ "snackbarAddedToQueue": "\"{trackName}\" hinzugefügt",
"@snackbarAddedToQueue": {
"description": "Snackbar - track added to download queue",
"placeholders": {
@@ -768,7 +768,7 @@
}
}
},
- "snackbarAddedTracksToQueue": "{count} Titel zur Warteschlange hinzugefügt",
+ "snackbarAddedTracksToQueue": "{count} Titel hinzugefügt",
"@snackbarAddedTracksToQueue": {
"description": "Snackbar - multiple tracks added to queue",
"placeholders": {
@@ -991,6 +991,14 @@
"@filenameFormat": {
"description": "Setting title - filename pattern"
},
+ "filenameShowAdvancedTags": "Erweiterte Tags anzeigen",
+ "@filenameShowAdvancedTags": {
+ "description": "Toggle label for showing advanced filename tags"
+ },
+ "filenameShowAdvancedTagsDescription": "Formatierte Tags für Track-Padding und Datumsmuster aktivieren",
+ "@filenameShowAdvancedTagsDescription": {
+ "description": "Description for advanced filename tag toggle"
+ },
"folderOrganizationNone": "Keine Organisation",
"@folderOrganizationNone": {
"description": "Folder option - flat structure"
@@ -1172,7 +1180,7 @@
}
}
},
- "logEntries": "Entries ({count})",
+ "logEntries": "{count} Einträge",
"@logEntries": {
"description": "Total log count",
"placeholders": {
@@ -1181,7 +1189,7 @@
}
}
},
- "credentialsTitle": "Spotify Credentials",
+ "credentialsTitle": "Spotify-Anmeldedaten",
"@credentialsTitle": {
"description": "Credentials dialog title"
},
@@ -1261,7 +1269,7 @@
"@lyricsModeDescription": {
"description": "Lyrics mode picker description"
},
- "lyricsModeEmbed": "In Datei einbinden",
+ "lyricsModeEmbed": "In Datei einbetten",
"@lyricsModeEmbed": {
"description": "Lyrics mode option - embed in audio file"
},
@@ -1281,7 +1289,7 @@
"@lyricsModeBoth": {
"description": "Lyrics mode option - embed and external"
},
- "lyricsModeBothSubtitle": "Lyrics einbinden und als .lrc speichern",
+ "lyricsModeBothSubtitle": "Lyrics einbetten und als .lrc speichern",
"@lyricsModeBothSubtitle": {
"description": "Subtitle for both option"
},
@@ -1305,35 +1313,35 @@
"@appearanceLanguage": {
"description": "Language setting title"
},
- "settingsAppearanceSubtitle": "Theme, colors, display",
+ "settingsAppearanceSubtitle": "Design, Farben, Anzeige",
"@settingsAppearanceSubtitle": {
"description": "Appearance settings description"
},
- "settingsDownloadSubtitle": "Service, quality, filename format",
+ "settingsDownloadSubtitle": "Dienst, Qualität, Dateinamen-Format",
"@settingsDownloadSubtitle": {
"description": "Download settings description"
},
- "settingsOptionsSubtitle": "Fallback, lyrics, cover art, updates",
+ "settingsOptionsSubtitle": "Fallback, Lyrics, Covers, Updates",
"@settingsOptionsSubtitle": {
"description": "Options settings description"
},
- "settingsExtensionsSubtitle": "Manage download providers",
+ "settingsExtensionsSubtitle": "Download-Anbieter verwalten",
"@settingsExtensionsSubtitle": {
"description": "Extensions settings description"
},
- "settingsLogsSubtitle": "View app logs for debugging",
+ "settingsLogsSubtitle": "App-Logs zum Debuggen anzeigen",
"@settingsLogsSubtitle": {
"description": "Logs settings description"
},
- "loadingSharedLink": "Loading shared link...",
+ "loadingSharedLink": "Link wird geladen...",
"@loadingSharedLink": {
"description": "Status when opening shared URL"
},
- "pressBackAgainToExit": "Press back again to exit",
+ "pressBackAgainToExit": "Drücke wieder \"zurück\" um die App zu beenden",
"@pressBackAgainToExit": {
"description": "Exit confirmation message"
},
- "downloadAllCount": "Download All ({count})",
+ "downloadAllCount": "Alle {count} Titel herunterladen",
"@downloadAllCount": {
"description": "Download all button with count",
"placeholders": {
@@ -1342,7 +1350,7 @@
}
}
},
- "tracksCount": "{count, plural, =1{1 track} other{{count} tracks}}",
+ "tracksCount": "{count, plural, =1{1 Titel} other{{count} Titel}}",
"@tracksCount": {
"description": "Track count display",
"placeholders": {
@@ -1351,23 +1359,23 @@
}
}
},
- "trackCopyFilePath": "Copy file path",
+ "trackCopyFilePath": "Dateipfad kopieren",
"@trackCopyFilePath": {
"description": "Action - copy file path"
},
- "trackRemoveFromDevice": "Remove from device",
+ "trackRemoveFromDevice": "Vom Gerät entfernen",
"@trackRemoveFromDevice": {
"description": "Action - delete downloaded file"
},
- "trackLoadLyrics": "Load Lyrics",
+ "trackLoadLyrics": "Lade Lyrics",
"@trackLoadLyrics": {
"description": "Action - fetch lyrics"
},
- "trackMetadata": "Metadata",
+ "trackMetadata": "Metadaten",
"@trackMetadata": {
"description": "Tab title - track metadata"
},
- "trackFileInfo": "File Info",
+ "trackFileInfo": "Datei-Info",
"@trackFileInfo": {
"description": "Tab title - file information"
},
@@ -1375,27 +1383,27 @@
"@trackLyrics": {
"description": "Tab title - lyrics"
},
- "trackFileNotFound": "File not found",
+ "trackFileNotFound": "Datei nicht gefunden",
"@trackFileNotFound": {
"description": "Error - file doesn't exist"
},
- "trackOpenInDeezer": "Open in Deezer",
+ "trackOpenInDeezer": "In Deezer öffnen",
"@trackOpenInDeezer": {
"description": "Action - open track in Deezer app"
},
- "trackOpenInSpotify": "Open in Spotify",
+ "trackOpenInSpotify": "In Spotify öffnen",
"@trackOpenInSpotify": {
"description": "Action - open track in Spotify app"
},
- "trackTrackName": "Track name",
+ "trackTrackName": "Name des Titels",
"@trackTrackName": {
"description": "Metadata label - track title"
},
- "trackArtist": "Artist",
+ "trackArtist": "Künstler",
"@trackArtist": {
"description": "Metadata label - artist name"
},
- "trackAlbumArtist": "Album artist",
+ "trackAlbumArtist": "Album Künstler",
"@trackAlbumArtist": {
"description": "Metadata label - album artist"
},
@@ -1403,23 +1411,23 @@
"@trackAlbum": {
"description": "Metadata label - album name"
},
- "trackTrackNumber": "Track number",
+ "trackTrackNumber": "Titelnummer",
"@trackTrackNumber": {
"description": "Metadata label - track number"
},
- "trackDiscNumber": "Disc number",
+ "trackDiscNumber": "CD-Nummer",
"@trackDiscNumber": {
"description": "Metadata label - disc number"
},
- "trackDuration": "Duration",
+ "trackDuration": "Länge",
"@trackDuration": {
"description": "Metadata label - track length"
},
- "trackAudioQuality": "Audio quality",
+ "trackAudioQuality": "Audioqualität",
"@trackAudioQuality": {
"description": "Metadata label - audio quality"
},
- "trackReleaseDate": "Release date",
+ "trackReleaseDate": "Erscheinungsdatum",
"@trackReleaseDate": {
"description": "Metadata label - release date"
},
@@ -1431,63 +1439,63 @@
"@trackLabel": {
"description": "Metadata label - record label"
},
- "trackCopyright": "Copyright",
+ "trackCopyright": "Urheberrecht",
"@trackCopyright": {
"description": "Metadata label - copyright information"
},
- "trackDownloaded": "Downloaded",
+ "trackDownloaded": "Heruntergeladen",
"@trackDownloaded": {
"description": "Metadata label - download date"
},
- "trackCopyLyrics": "Copy lyrics",
+ "trackCopyLyrics": "Lyrics kopieren",
"@trackCopyLyrics": {
"description": "Action - copy lyrics to clipboard"
},
- "trackLyricsNotAvailable": "Lyrics not available for this track",
+ "trackLyricsNotAvailable": "Lyrics sind für diesen Titel nicht verfügbar",
"@trackLyricsNotAvailable": {
"description": "Message when lyrics not found"
},
- "trackLyricsTimeout": "Request timed out. Try again later.",
+ "trackLyricsTimeout": "Anfrage Timeout. Versuche es später erneut.",
"@trackLyricsTimeout": {
"description": "Message when lyrics request times out"
},
- "trackLyricsLoadFailed": "Failed to load lyrics",
+ "trackLyricsLoadFailed": "Fehler beim Laden der Lyrics",
"@trackLyricsLoadFailed": {
"description": "Message when lyrics loading fails"
},
- "trackEmbedLyrics": "Embed Lyrics",
+ "trackEmbedLyrics": "Lyrics einbetten",
"@trackEmbedLyrics": {
"description": "Action - embed lyrics into audio file"
},
- "trackLyricsEmbedded": "Lyrics embedded successfully",
+ "trackLyricsEmbedded": "Lyrics erfolgreich eingebettet",
"@trackLyricsEmbedded": {
"description": "Snackbar - lyrics saved to file"
},
- "trackInstrumental": "Instrumental track",
+ "trackInstrumental": "Instrumentalspur",
"@trackInstrumental": {
"description": "Message when track is instrumental (no lyrics)"
},
- "trackCopiedToClipboard": "Copied to clipboard",
+ "trackCopiedToClipboard": "In Zwischenablage kopiert",
"@trackCopiedToClipboard": {
"description": "Snackbar - content copied"
},
- "trackDeleteConfirmTitle": "Remove from device?",
+ "trackDeleteConfirmTitle": "Vom Gerät entfernen?",
"@trackDeleteConfirmTitle": {
"description": "Delete confirmation title"
},
- "trackDeleteConfirmMessage": "This will permanently delete the downloaded file and remove it from your history.",
+ "trackDeleteConfirmMessage": "Dies wird die heruntergeladene Datei dauerhaft löschen und sie aus deinem Verlauf entfernen.",
"@trackDeleteConfirmMessage": {
"description": "Delete confirmation message"
},
- "dateToday": "Today",
+ "dateToday": "Heute",
"@dateToday": {
"description": "Relative date - today"
},
- "dateYesterday": "Yesterday",
+ "dateYesterday": "Gestern",
"@dateYesterday": {
"description": "Relative date - yesterday"
},
- "dateDaysAgo": "{count} days ago",
+ "dateDaysAgo": "Vor {count} Tagen",
"@dateDaysAgo": {
"description": "Relative date - days ago",
"placeholders": {
@@ -1496,7 +1504,7 @@
}
}
},
- "dateWeeksAgo": "{count} weeks ago",
+ "dateWeeksAgo": "Vor {count} Wochen",
"@dateWeeksAgo": {
"description": "Relative date - weeks ago",
"placeholders": {
@@ -1505,7 +1513,7 @@
}
}
},
- "dateMonthsAgo": "{count} months ago",
+ "dateMonthsAgo": "Vor {count} Monaten",
"@dateMonthsAgo": {
"description": "Relative date - months ago",
"placeholders": {
@@ -1514,15 +1522,15 @@
}
}
},
- "storeFilterAll": "All",
+ "storeFilterAll": "Alle",
"@storeFilterAll": {
"description": "Store filter - all extensions"
},
- "storeFilterMetadata": "Metadata",
+ "storeFilterMetadata": "Metadaten",
"@storeFilterMetadata": {
"description": "Store filter - metadata providers"
},
- "storeFilterDownload": "Download",
+ "storeFilterDownload": "Herunterladen",
"@storeFilterDownload": {
"description": "Store filter - download providers"
},
@@ -1538,19 +1546,19 @@
"@storeFilterIntegration": {
"description": "Store filter - integrations"
},
- "storeClearFilters": "Clear filters",
+ "storeClearFilters": "Filter entfernen",
"@storeClearFilters": {
"description": "Button to clear all filters"
},
- "extensionDefaultProvider": "Default (Deezer/Spotify)",
+ "extensionDefaultProvider": "Standard (Deezer/Spotify)",
"@extensionDefaultProvider": {
"description": "Default search provider option"
},
- "extensionDefaultProviderSubtitle": "Use built-in search",
+ "extensionDefaultProviderSubtitle": "Eingebaute Suche verwenden",
"@extensionDefaultProviderSubtitle": {
"description": "Subtitle for default provider"
},
- "extensionAuthor": "Author",
+ "extensionAuthor": "Entwickler",
"@extensionAuthor": {
"description": "Extension detail - author"
},
@@ -1558,23 +1566,23 @@
"@extensionId": {
"description": "Extension detail - unique ID"
},
- "extensionError": "Error",
+ "extensionError": "Fehler",
"@extensionError": {
"description": "Extension detail - error message"
},
- "extensionCapabilities": "Capabilities",
+ "extensionCapabilities": "Eigenschaften",
"@extensionCapabilities": {
"description": "Section header - extension features"
},
- "extensionMetadataProvider": "Metadata Provider",
+ "extensionMetadataProvider": "Metadaten-Anbieter",
"@extensionMetadataProvider": {
"description": "Capability - provides metadata"
},
- "extensionDownloadProvider": "Download Provider",
+ "extensionDownloadProvider": "Download-Anbieter",
"@extensionDownloadProvider": {
"description": "Capability - provides downloads"
},
- "extensionLyricsProvider": "Lyrics Provider",
+ "extensionLyricsProvider": "Lyrics-Anbieter",
"@extensionLyricsProvider": {
"description": "Capability - provides lyrics"
},
@@ -1582,7 +1590,7 @@
"@extensionUrlHandler": {
"description": "Capability - handles URLs"
},
- "extensionQualityOptions": "Quality Options",
+ "extensionQualityOptions": "Qualitätsoptionen",
"@extensionQualityOptions": {
"description": "Capability - quality selection"
},
@@ -1590,35 +1598,35 @@
"@extensionPostProcessingHooks": {
"description": "Capability - post-processing"
},
- "extensionPermissions": "Permissions",
+ "extensionPermissions": "Berechtigungen",
"@extensionPermissions": {
"description": "Section header - required permissions"
},
- "extensionSettings": "Settings",
+ "extensionSettings": "Einstellungen",
"@extensionSettings": {
"description": "Section header - extension settings"
},
- "extensionRemoveButton": "Remove Extension",
+ "extensionRemoveButton": "Erweiterung entfernen",
"@extensionRemoveButton": {
"description": "Button to uninstall extension"
},
- "extensionUpdated": "Updated",
+ "extensionUpdated": "Aktualisiert",
"@extensionUpdated": {
"description": "Extension detail - last update"
},
- "extensionMinAppVersion": "Min App Version",
+ "extensionMinAppVersion": "Min App-Version",
"@extensionMinAppVersion": {
"description": "Extension detail - minimum app version"
},
- "extensionCustomTrackMatching": "Custom Track Matching",
+ "extensionCustomTrackMatching": "Benutzerdefiniertes Track-Matching",
"@extensionCustomTrackMatching": {
"description": "Capability - custom track matching algorithm"
},
- "extensionPostProcessing": "Post-Processing",
+ "extensionPostProcessing": "Post-processing",
"@extensionPostProcessing": {
"description": "Capability - post-download processing"
},
- "extensionHooksAvailable": "{count} hook(s) available",
+ "extensionHooksAvailable": "{count} Hook(s) verfügbar",
"@extensionHooksAvailable": {
"description": "Post-processing hooks count",
"placeholders": {
@@ -1627,7 +1635,7 @@
}
}
},
- "extensionPatternsCount": "{count} pattern(s)",
+ "extensionPatternsCount": "{count} Muster",
"@extensionPatternsCount": {
"description": "URL patterns count",
"placeholders": {
@@ -1636,7 +1644,7 @@
}
}
},
- "extensionStrategy": "Strategy: {strategy}",
+ "extensionStrategy": "Strategie: {strategy}",
"@extensionStrategy": {
"description": "Track matching strategy name",
"placeholders": {
@@ -1645,79 +1653,79 @@
}
}
},
- "extensionsProviderPrioritySection": "Provider Priority",
+ "extensionsProviderPrioritySection": "Provider-Priorität",
"@extensionsProviderPrioritySection": {
"description": "Section header - provider priority"
},
- "extensionsInstalledSection": "Installed Extensions",
+ "extensionsInstalledSection": "Installierte Erweiterungen",
"@extensionsInstalledSection": {
"description": "Section header - installed extensions"
},
- "extensionsNoExtensions": "No extensions installed",
+ "extensionsNoExtensions": "Keine Erweiterungen installiert",
"@extensionsNoExtensions": {
"description": "Empty state - no extensions"
},
- "extensionsNoExtensionsSubtitle": "Install .spotiflac-ext files to add new providers",
+ "extensionsNoExtensionsSubtitle": "Installiere .spotiflac-ext Dateien um neue Anbieter hinzuzufügen",
"@extensionsNoExtensionsSubtitle": {
"description": "Empty state subtitle"
},
- "extensionsInstallButton": "Install Extension",
+ "extensionsInstallButton": "Erweiterung installieren",
"@extensionsInstallButton": {
"description": "Button to install extension from file"
},
- "extensionsInfoTip": "Extensions can add new metadata and download providers. Only install extensions from trusted sources.",
+ "extensionsInfoTip": "Erweiterungen können neue Metadaten und Download-Anbieter hinzufügen. Installiere nur Erweiterungen von vertrauenswürdigen Quellen.",
"@extensionsInfoTip": {
"description": "Security warning about extensions"
},
- "extensionsInstalledSuccess": "Extension installed successfully",
+ "extensionsInstalledSuccess": "Erweiterung erfolgreich installiert",
"@extensionsInstalledSuccess": {
"description": "Success message after install"
},
- "extensionsDownloadPriority": "Download Priority",
+ "extensionsDownloadPriority": "Download-Priorität",
"@extensionsDownloadPriority": {
"description": "Setting - download provider order"
},
- "extensionsDownloadPrioritySubtitle": "Set download service order",
+ "extensionsDownloadPrioritySubtitle": "Download-Service-Reihenfolge festlegen",
"@extensionsDownloadPrioritySubtitle": {
"description": "Subtitle for download priority"
},
- "extensionsNoDownloadProvider": "No extensions with download provider",
+ "extensionsNoDownloadProvider": "Keine Erweiterungen mit Download-Provider",
"@extensionsNoDownloadProvider": {
"description": "Empty state - no download providers"
},
- "extensionsMetadataPriority": "Metadata Priority",
+ "extensionsMetadataPriority": "Metadaten Priorität",
"@extensionsMetadataPriority": {
"description": "Setting - metadata provider order"
},
- "extensionsMetadataPrioritySubtitle": "Set search & metadata source order",
+ "extensionsMetadataPrioritySubtitle": "Reihenfolge der Such- und Metadaten quellen festlegen",
"@extensionsMetadataPrioritySubtitle": {
"description": "Subtitle for metadata priority"
},
- "extensionsNoMetadataProvider": "No extensions with metadata provider",
+ "extensionsNoMetadataProvider": "Keine Erweiterungen mit Metadaten-Anbieter",
"@extensionsNoMetadataProvider": {
"description": "Empty state - no metadata providers"
},
- "extensionsSearchProvider": "Search Provider",
+ "extensionsSearchProvider": "Such-Provider",
"@extensionsSearchProvider": {
"description": "Setting - search provider selection"
},
- "extensionsNoCustomSearch": "No extensions with custom search",
+ "extensionsNoCustomSearch": "Keine Erweiterungen mit benutzerdefinierter Suche",
"@extensionsNoCustomSearch": {
"description": "Empty state - no search providers"
},
- "extensionsSearchProviderDescription": "Choose which service to use for searching tracks",
+ "extensionsSearchProviderDescription": "Wähle den Dienst für die Suche von Titel",
"@extensionsSearchProviderDescription": {
"description": "Search provider setting description"
},
- "extensionsCustomSearch": "Custom search",
+ "extensionsCustomSearch": "Benutzerdefinierte Suche",
"@extensionsCustomSearch": {
"description": "Label for custom search provider"
},
- "extensionsErrorLoading": "Error loading extension",
+ "extensionsErrorLoading": "Fehler beim Laden der Erweiterung",
"@extensionsErrorLoading": {
"description": "Error message when extension fails to load"
},
- "qualityFlacLossless": "FLAC Lossless",
+ "qualityFlacLossless": "FLAC Verlustfrei",
"@qualityFlacLossless": {
"description": "Quality option - CD quality FLAC"
},
@@ -1729,7 +1737,7 @@
"@qualityHiResFlac": {
"description": "Quality option - high resolution FLAC"
},
- "qualityHiResFlacSubtitle": "24-bit / up to 96kHz",
+ "qualityHiResFlacSubtitle": "24-Bit / bis 96kHz",
"@qualityHiResFlacSubtitle": {
"description": "Technical spec for hi-res"
},
@@ -1737,27 +1745,35 @@
"@qualityHiResFlacMax": {
"description": "Quality option - maximum resolution FLAC"
},
- "qualityHiResFlacMaxSubtitle": "24-bit / up to 192kHz",
+ "qualityHiResFlacMaxSubtitle": "24-Bit / bis 192kHz",
"@qualityHiResFlacMaxSubtitle": {
"description": "Technical spec for hi-res max"
},
- "qualityNote": "Actual quality depends on track availability from the service",
+ "qualityNote": "Die eigentliche Qualität hängt von der Verfügbarkeit des Dienstes ab",
"@qualityNote": {
"description": "Note about quality availability"
},
- "youtubeQualityNote": "YouTube provides lossy audio only. Not part of lossless fallback.",
+ "youtubeQualityNote": "YouTube bietet nur verlustbehaftete Audioqualität. Deswegen ist es kein Teil des verlustfreien Fallbacks.",
"@youtubeQualityNote": {
"description": "Note for YouTube service explaining lossy-only quality"
},
- "downloadAskBeforeDownload": "Ask Before Download",
+ "youtubeOpusBitrateTitle": "YouTube Opus Bitrate",
+ "@youtubeOpusBitrateTitle": {
+ "description": "Title for YouTube Opus bitrate setting"
+ },
+ "youtubeMp3BitrateTitle": "YouTube MP3 Bitrate",
+ "@youtubeMp3BitrateTitle": {
+ "description": "Title for YouTube MP3 bitrate setting"
+ },
+ "downloadAskBeforeDownload": "Qualität vor Download fragen",
"@downloadAskBeforeDownload": {
"description": "Setting - show quality picker"
},
- "downloadDirectory": "Download Directory",
+ "downloadDirectory": "Downloadverzeichnis",
"@downloadDirectory": {
"description": "Setting - download folder"
},
- "downloadSeparateSinglesFolder": "Separate Singles Folder",
+ "downloadSeparateSinglesFolder": "Singles Ordner trennen",
"@downloadSeparateSinglesFolder": {
"description": "Setting - separate folder for singles"
},
@@ -1773,7 +1789,7 @@
"@downloadUsePrimaryArtistOnly": {
"description": "Setting - strip featured artists from folder name"
},
- "downloadUsePrimaryArtistOnlyEnabled": "Featured artists removed from folder name (e.g. Justin Bieber, Quavo → Justin Bieber)",
+ "downloadUsePrimaryArtistOnlyEnabled": "Vorgestellte Künstler aus dem Ordnernamen entfernt (z.B. Justin Bieber, Quavo → Justin Bieber)",
"@downloadUsePrimaryArtistOnlyEnabled": {
"description": "Subtitle when primary artist only is enabled"
},
@@ -1781,27 +1797,27 @@
"@downloadUsePrimaryArtistOnlyDisabled": {
"description": "Subtitle when primary artist only is disabled"
},
- "downloadSelectQuality": "Select Quality",
+ "downloadSelectQuality": "Qualität wählen",
"@downloadSelectQuality": {
"description": "Dialog title - choose audio quality"
},
- "downloadFrom": "Download From",
+ "downloadFrom": "Herunterladen von",
"@downloadFrom": {
"description": "Label - download source"
},
- "appearanceAmoledDark": "AMOLED Dark",
+ "appearanceAmoledDark": "AMOLED Schwarz",
"@appearanceAmoledDark": {
"description": "Theme option - pure black"
},
- "appearanceAmoledDarkSubtitle": "Pure black background",
+ "appearanceAmoledDarkSubtitle": "AMOLED Hintergrund",
"@appearanceAmoledDarkSubtitle": {
"description": "Subtitle for AMOLED dark"
},
- "queueClearAll": "Clear All",
+ "queueClearAll": "Alles löschen",
"@queueClearAll": {
"description": "Button - clear all queue items"
},
- "queueClearAllMessage": "Are you sure you want to clear all downloads?",
+ "queueClearAllMessage": "Bist du dir sicher, dass du alle Downloads löschen möchten?",
"@queueClearAllMessage": {
"description": "Clear queue confirmation"
},
@@ -1809,27 +1825,27 @@
"@settingsAutoExportFailed": {
"description": "Setting toggle for auto-export"
},
- "settingsAutoExportFailedSubtitle": "Save failed downloads to TXT file automatically",
+ "settingsAutoExportFailedSubtitle": "Fehlgeschlagene Downloads automatisch in eine TXT-Datei speichern",
"@settingsAutoExportFailedSubtitle": {
"description": "Subtitle for auto-export setting"
},
- "settingsDownloadNetwork": "Download Network",
+ "settingsDownloadNetwork": "Download Netzwerk",
"@settingsDownloadNetwork": {
"description": "Setting for network type preference"
},
- "settingsDownloadNetworkAny": "WiFi + Mobile Data",
+ "settingsDownloadNetworkAny": "WLAN + Mobile Daten",
"@settingsDownloadNetworkAny": {
"description": "Network option - use any connection"
},
- "settingsDownloadNetworkWifiOnly": "WiFi Only",
+ "settingsDownloadNetworkWifiOnly": "Nur WLAN",
"@settingsDownloadNetworkWifiOnly": {
"description": "Network option - only use WiFi"
},
- "settingsDownloadNetworkSubtitle": "Choose which network to use for downloads. When set to WiFi Only, downloads will pause on mobile data.",
+ "settingsDownloadNetworkSubtitle": "Wähle aus, welches Netzwerk für Downloads verwendet werden soll. Wenn nur WLAN aktiviert wird, werden Downloads auf mobilen Daten angehalten.",
"@settingsDownloadNetworkSubtitle": {
"description": "Subtitle explaining network preference"
},
- "albumFolderArtistAlbum": "Artist / Album",
+ "albumFolderArtistAlbum": "Künstler/Album",
"@albumFolderArtistAlbum": {
"description": "Album folder option"
},
@@ -1841,15 +1857,15 @@
"@albumFolderArtistYearAlbum": {
"description": "Album folder option with year"
},
- "albumFolderArtistYearAlbumSubtitle": "Albums/Artist Name/[2005] Album Name/",
+ "albumFolderArtistYearAlbumSubtitle": "Albums/Künster Name/[2005] Album Name/",
"@albumFolderArtistYearAlbumSubtitle": {
"description": "Folder structure example"
},
- "albumFolderAlbumOnly": "Album Only",
+ "albumFolderAlbumOnly": "Nur Alben",
"@albumFolderAlbumOnly": {
"description": "Album folder option"
},
- "albumFolderAlbumOnlySubtitle": "Albums/Album Name/",
+ "albumFolderAlbumOnlySubtitle": "Alben/Album Name/",
"@albumFolderAlbumOnlySubtitle": {
"description": "Folder structure example"
},
@@ -1869,11 +1885,11 @@
"@albumFolderArtistAlbumSinglesSubtitle": {
"description": "Folder structure example"
},
- "downloadedAlbumDeleteSelected": "Delete Selected",
+ "downloadedAlbumDeleteSelected": "Ausgewählte löschen",
"@downloadedAlbumDeleteSelected": {
"description": "Button - delete selected tracks"
},
- "downloadedAlbumDeleteMessage": "Delete {count} {count, plural, =1{track} other{tracks}} from this album?\n\nThis will also delete the files from storage.",
+ "downloadedAlbumDeleteMessage": "{count} {count, plural, one {Titel} other{Titel}} aus diesem Album löschen?\n\nDadurch werden auch die Dateien aus dem Speicher gelöscht.",
"@downloadedAlbumDeleteMessage": {
"description": "Delete confirmation with count",
"placeholders": {
@@ -1882,7 +1898,7 @@
}
}
},
- "downloadedAlbumSelectedCount": "{count} selected",
+ "downloadedAlbumSelectedCount": "{count} ausgewählt",
"@downloadedAlbumSelectedCount": {
"description": "Selection count indicator",
"placeholders": {
@@ -1891,15 +1907,15 @@
}
}
},
- "downloadedAlbumAllSelected": "All tracks selected",
+ "downloadedAlbumAllSelected": "Alle Titel sind ausgewählt",
"@downloadedAlbumAllSelected": {
"description": "Status - all items selected"
},
- "downloadedAlbumTapToSelect": "Tap tracks to select",
+ "downloadedAlbumTapToSelect": "Tippe auf Titel zum Auswählen",
"@downloadedAlbumTapToSelect": {
"description": "Selection hint"
},
- "downloadedAlbumDeleteCount": "Delete {count} {count, plural, =1{track} other{tracks}}",
+ "downloadedAlbumDeleteCount": "Lösche {count} {count, plural, one {Titel} other{Titel}}",
"@downloadedAlbumDeleteCount": {
"description": "Delete button text with count",
"placeholders": {
@@ -1922,7 +1938,7 @@
}
}
},
- "recentTypeArtist": "Artist",
+ "recentTypeArtist": "Künstler",
"@recentTypeArtist": {
"description": "Recent access item type - artist"
},
@@ -1930,7 +1946,7 @@
"@recentTypeAlbum": {
"description": "Recent access item type - album"
},
- "recentTypeSong": "Song",
+ "recentTypeSong": "Titel",
"@recentTypeSong": {
"description": "Recent access item type - song/track"
},
@@ -1938,11 +1954,11 @@
"@recentTypePlaylist": {
"description": "Recent access item type - playlist"
},
- "recentEmpty": "No recent items yet",
+ "recentEmpty": "Noch keine aktuellen Einträge",
"@recentEmpty": {
"description": "Empty state text for recent access list"
},
- "recentShowAllDownloads": "Show All Downloads",
+ "recentShowAllDownloads": "Alle Downloads anzeigen",
"@recentShowAllDownloads": {
"description": "Button label to unhide hidden downloads in recent access"
},
@@ -1956,15 +1972,15 @@
}
}
},
- "discographyDownload": "Download Discography",
+ "discographyDownload": "Diskographie herunterladen",
"@discographyDownload": {
"description": "Button - download artist discography"
},
- "discographyDownloadAll": "Download All",
+ "discographyDownloadAll": "Alle Herunterladen",
"@discographyDownloadAll": {
"description": "Option - download entire discography"
},
- "discographyDownloadAllSubtitle": "{count} tracks from {albumCount} releases",
+ "discographyDownloadAllSubtitle": "{count} Titel von {albumCount} Releases",
"@discographyDownloadAllSubtitle": {
"description": "Subtitle showing total tracks and albums",
"placeholders": {
@@ -1976,11 +1992,11 @@
}
}
},
- "discographyAlbumsOnly": "Albums Only",
+ "discographyAlbumsOnly": "Nur Alben",
"@discographyAlbumsOnly": {
"description": "Option - download only albums"
},
- "discographyAlbumsOnlySubtitle": "{count} tracks from {albumCount} albums",
+ "discographyAlbumsOnlySubtitle": "{count} Titel von {albumCount} Albums",
"@discographyAlbumsOnlySubtitle": {
"description": "Subtitle showing album tracks count",
"placeholders": {
@@ -1992,11 +2008,11 @@
}
}
},
- "discographySinglesOnly": "Singles & EPs Only",
+ "discographySinglesOnly": "Nur Singles & EPs",
"@discographySinglesOnly": {
"description": "Option - download only singles"
},
- "discographySinglesOnlySubtitle": "{count} tracks from {albumCount} singles",
+ "discographySinglesOnlySubtitle": "{count} Titel von {albumCount} Singles",
"@discographySinglesOnlySubtitle": {
"description": "Subtitle showing singles tracks count",
"placeholders": {
@@ -2008,7 +2024,7 @@
}
}
},
- "discographySelectAlbums": "Select Albums...",
+ "discographySelectAlbums": "Alben auswählen...",
"@discographySelectAlbums": {
"description": "Option - manually select albums to download"
},
@@ -2016,7 +2032,7 @@
"@discographySelectAlbumsSubtitle": {
"description": "Subtitle for select albums option"
},
- "discographyFetchingTracks": "Fetching tracks...",
+ "discographyFetchingTracks": "Lade Titel...",
"@discographyFetchingTracks": {
"description": "Progress - fetching album tracks"
},
@@ -2032,7 +2048,7 @@
}
}
},
- "discographySelectedCount": "{count} selected",
+ "discographySelectedCount": "{count} ausgewählt",
"@discographySelectedCount": {
"description": "Selection count badge",
"placeholders": {
@@ -2041,7 +2057,7 @@
}
}
},
- "discographyDownloadSelected": "Download Selected",
+ "discographyDownloadSelected": "Auswahl herunterladen",
"@discographyDownloadSelected": {
"description": "Button - download selected albums"
},
@@ -2054,7 +2070,7 @@
}
}
},
- "discographySkippedDownloaded": "{added} added, {skipped} already downloaded",
+ "discographySkippedDownloaded": "{added} hinzugefügt, {skipped} bereits heruntergeladen",
"@discographySkippedDownloaded": {
"description": "Snackbar - with skipped tracks count",
"placeholders": {
@@ -2066,7 +2082,7 @@
}
}
},
- "discographyNoAlbums": "No albums available",
+ "discographyNoAlbums": "Es sind keine Alben verfügbar",
"@discographyNoAlbums": {
"description": "Error - no albums found for artist"
},
@@ -2074,11 +2090,11 @@
"@discographyFailedToFetch": {
"description": "Error - some albums failed to load"
},
- "sectionStorageAccess": "Storage Access",
+ "sectionStorageAccess": "Speicherzugriff",
"@sectionStorageAccess": {
"description": "Section header for storage access settings"
},
- "allFilesAccess": "All Files Access",
+ "allFilesAccess": "Zugriff auf alle Dateien",
"@allFilesAccess": {
"description": "Toggle for MANAGE_EXTERNAL_STORAGE permission"
},
@@ -2090,19 +2106,19 @@
"@allFilesAccessDisabledSubtitle": {
"description": "Subtitle when all files access is disabled"
},
- "allFilesAccessDescription": "Enable this if you encounter write errors when saving to custom folders. Android 13+ restricts access to certain directories by default.",
+ "allFilesAccessDescription": "Aktiviere die Option, wenn beim Speichern in benutzerdefinierten Ordnern Schreibfehler auftreten. Weil Android 13+ standardmäßig den Zugriff auf bestimmte Verzeichnisse einschränkt.",
"@allFilesAccessDescription": {
"description": "Description explaining when to enable all files access"
},
- "allFilesAccessDeniedMessage": "Permission was denied. Please enable 'All files access' manually in system settings.",
+ "allFilesAccessDeniedMessage": "Zugriff verweigert. Bitte aktiviere \"Zugriff auf alle Dateien\" manuell in den Systemeinstellungen.",
"@allFilesAccessDeniedMessage": {
"description": "Message when permission is permanently denied"
},
- "allFilesAccessDisabledMessage": "All Files Access disabled. The app will use limited storage access.",
+ "allFilesAccessDisabledMessage": "Zugriff auf alle Dateien ist deaktiviert. Die App verwendet nur begrenzten Zugriff auf den Speicher.",
"@allFilesAccessDisabledMessage": {
"description": "Snackbar message when user disables all files access"
},
- "settingsLocalLibrary": "Local Library",
+ "settingsLocalLibrary": "Lokale Bibliothek",
"@settingsLocalLibrary": {
"description": "Settings menu item - local library"
},
@@ -2110,7 +2126,7 @@
"@settingsLocalLibrarySubtitle": {
"description": "Subtitle for local library settings"
},
- "settingsCache": "Storage & Cache",
+ "settingsCache": "Speicher & Cache",
"@settingsCache": {
"description": "Settings menu item - cache management"
},
@@ -2118,15 +2134,15 @@
"@settingsCacheSubtitle": {
"description": "Subtitle for cache management menu"
},
- "libraryTitle": "Local Library",
+ "libraryTitle": "Lokale Bibliothek",
"@libraryTitle": {
"description": "Library settings page title"
},
- "libraryScanSettings": "Scan Settings",
+ "libraryScanSettings": "Scan Einstellungen",
"@libraryScanSettings": {
"description": "Section header for scan settings"
},
- "libraryEnableLocalLibrary": "Enable Local Library",
+ "libraryEnableLocalLibrary": "Lokale Bibliothek aktivieren",
"@libraryEnableLocalLibrary": {
"description": "Toggle to enable library scanning"
},
@@ -2134,11 +2150,11 @@
"@libraryEnableLocalLibrarySubtitle": {
"description": "Subtitle for enable toggle"
},
- "libraryFolder": "Library Folder",
+ "libraryFolder": "Bibliotheksordner",
"@libraryFolder": {
"description": "Folder selection setting"
},
- "libraryFolderHint": "Tap to select folder",
+ "libraryFolderHint": "Tippe um Ordner auszuwählen",
"@libraryFolderHint": {
"description": "Placeholder when no folder selected"
},
@@ -2146,59 +2162,68 @@
"@libraryShowDuplicateIndicator": {
"description": "Toggle for duplicate indicator in search"
},
- "libraryShowDuplicateIndicatorSubtitle": "Show when searching for existing tracks",
+ "libraryShowDuplicateIndicatorSubtitle": "Bei der Suche nach vorhandenen Titeln anzeigen",
"@libraryShowDuplicateIndicatorSubtitle": {
"description": "Subtitle for duplicate indicator toggle"
},
- "libraryActions": "Actions",
+ "libraryActions": "Aktionen",
"@libraryActions": {
"description": "Section header for library actions"
},
- "libraryScan": "Scan Library",
+ "libraryScan": "Bibliothek scannen",
"@libraryScan": {
"description": "Button to start library scan"
},
- "libraryScanSubtitle": "Scan for audio files",
+ "libraryScanSubtitle": "Suche nach Audiodateien",
"@libraryScanSubtitle": {
"description": "Subtitle for scan button"
},
- "libraryScanSelectFolderFirst": "Select a folder first",
+ "libraryScanSelectFolderFirst": "Wähle zuerst einen Ordner",
"@libraryScanSelectFolderFirst": {
"description": "Message when trying to scan without folder"
},
- "libraryCleanupMissingFiles": "Cleanup Missing Files",
+ "libraryCleanupMissingFiles": "Fehlende Dateien bereinigen",
"@libraryCleanupMissingFiles": {
"description": "Button to remove entries for missing files"
},
- "libraryCleanupMissingFilesSubtitle": "Remove entries for files that no longer exist",
+ "libraryCleanupMissingFilesSubtitle": "Verlaufseinträge für Dateien löschen, die nicht mehr existieren",
"@libraryCleanupMissingFilesSubtitle": {
"description": "Subtitle for cleanup button"
},
- "libraryClear": "Clear Library",
+ "libraryClear": "Bibliothek löschen",
"@libraryClear": {
"description": "Button to clear all library entries"
},
- "libraryClearSubtitle": "Remove all scanned tracks",
+ "libraryClearSubtitle": "Alle gescannten Titel entfernen",
"@libraryClearSubtitle": {
"description": "Subtitle for clear button"
},
- "libraryClearConfirmTitle": "Clear Library",
+ "libraryClearConfirmTitle": "Bibliothek löschen",
"@libraryClearConfirmTitle": {
"description": "Dialog title for clear confirmation"
},
- "libraryClearConfirmMessage": "This will remove all scanned tracks from your library. Your actual music files will not be deleted.",
+ "libraryClearConfirmMessage": "Dadurch werden alle gescannten Titel aus Ihrer Bibliothek entfernt. Ihre eigentlichen Musikdateien werden nicht gelöscht.",
"@libraryClearConfirmMessage": {
"description": "Dialog message for clear confirmation"
},
- "libraryAbout": "About Local Library",
+ "libraryAbout": "Über die lokale Bibliothek",
"@libraryAbout": {
"description": "Section header for about info"
},
- "libraryAboutDescription": "Scans your existing music collection to detect duplicates when downloading. Supports FLAC, M4A, MP3, Opus, and OGG formats. Metadata is read from file tags when available.",
+ "libraryAboutDescription": "Durchsucht deine bestehende Musiksammlung, um Duplikate beim Herunterladen zu erkennen. Unterstützt die Formate FLAC, M4A, MP3, Opus und OGG. Metadaten werden, sofern verfügbar, aus den Dateitags gelesen.",
"@libraryAboutDescription": {
"description": "Description of local library feature"
},
- "libraryLastScanned": "Last scanned: {time}",
+ "libraryTracksUnit": "{count, plural, =1{1 Titel} other{{count} Titel}}",
+ "@libraryTracksUnit": {
+ "description": "Unit label for tracks count (without the number itself)",
+ "placeholders": {
+ "count": {
+ "type": "int"
+ }
+ }
+ },
+ "libraryLastScanned": "Zuletzt gescannt: {time}",
"@libraryLastScanned": {
"description": "Last scan time display",
"placeholders": {
@@ -2207,15 +2232,15 @@
}
}
},
- "libraryLastScannedNever": "Never",
+ "libraryLastScannedNever": "Nie",
"@libraryLastScannedNever": {
"description": "Shown when library has never been scanned"
},
- "libraryScanning": "Scanning...",
+ "libraryScanning": "Scannen...",
"@libraryScanning": {
"description": "Status during scan"
},
- "libraryScanProgress": "{progress}% of {total} files",
+ "libraryScanProgress": "{progress}% von {total} Dateien",
"@libraryScanProgress": {
"description": "Scan progress display",
"placeholders": {
@@ -2227,11 +2252,11 @@
}
}
},
- "libraryInLibrary": "In Library",
+ "libraryInLibrary": "In Bibliothek",
"@libraryInLibrary": {
"description": "Badge shown on tracks that exist in local library"
},
- "libraryRemovedMissingFiles": "Removed {count} missing files from library",
+ "libraryRemovedMissingFiles": "Entfernte {count} fehlende Dateien aus der Bibliothek",
"@libraryRemovedMissingFiles": {
"description": "Snackbar after cleanup",
"placeholders": {
@@ -2240,59 +2265,59 @@
}
}
},
- "libraryCleared": "Library cleared",
+ "libraryCleared": "Bibliothek geleert",
"@libraryCleared": {
"description": "Snackbar after clearing library"
},
- "libraryStorageAccessRequired": "Storage Access Required",
+ "libraryStorageAccessRequired": "Speicherzugriff erforderlich",
"@libraryStorageAccessRequired": {
"description": "Dialog title for storage permission"
},
- "libraryStorageAccessMessage": "SpotiFLAC needs storage access to scan your music library. Please grant permission in settings.",
+ "libraryStorageAccessMessage": "SpotiFLAC benötigt Speicherzugriff, um deine Musikbibliothek zu scannen. Bitte erteile die Berechtigung in den Einstellungen.",
"@libraryStorageAccessMessage": {
"description": "Dialog message for storage permission"
},
- "libraryFolderNotExist": "Selected folder does not exist",
+ "libraryFolderNotExist": "Der ausgewählte Ordner existiert nicht",
"@libraryFolderNotExist": {
"description": "Error when folder doesn't exist"
},
- "librarySourceDownloaded": "Downloaded",
+ "librarySourceDownloaded": "Heruntergeladen",
"@librarySourceDownloaded": {
"description": "Badge for tracks downloaded via SpotiFLAC"
},
- "librarySourceLocal": "Local",
+ "librarySourceLocal": "Lokal",
"@librarySourceLocal": {
"description": "Badge for tracks from local library scan"
},
- "libraryFilterAll": "All",
+ "libraryFilterAll": "Alle",
"@libraryFilterAll": {
"description": "Filter chip - show all library items"
},
- "libraryFilterDownloaded": "Downloaded",
+ "libraryFilterDownloaded": "Heruntergeladen",
"@libraryFilterDownloaded": {
"description": "Filter chip - show only downloaded items"
},
- "libraryFilterLocal": "Local",
+ "libraryFilterLocal": "Lokal",
"@libraryFilterLocal": {
"description": "Filter chip - show only local library items"
},
- "libraryFilterTitle": "Filters",
+ "libraryFilterTitle": "Filter",
"@libraryFilterTitle": {
"description": "Filter bottom sheet title"
},
- "libraryFilterReset": "Reset",
+ "libraryFilterReset": "Zurücksetzen",
"@libraryFilterReset": {
"description": "Reset all filters button"
},
- "libraryFilterApply": "Apply",
+ "libraryFilterApply": "Anwenden",
"@libraryFilterApply": {
"description": "Apply filters button"
},
- "libraryFilterSource": "Source",
+ "libraryFilterSource": "Quelle",
"@libraryFilterSource": {
"description": "Filter section - source type"
},
- "libraryFilterQuality": "Quality",
+ "libraryFilterQuality": "Qualität",
"@libraryFilterQuality": {
"description": "Filter section - audio quality"
},
@@ -2304,7 +2329,7 @@
"@libraryFilterQualityCD": {
"description": "Filter option - CD quality audio"
},
- "libraryFilterQualityLossy": "Lossy",
+ "libraryFilterQualityLossy": "Verlustbehaftet",
"@libraryFilterQualityLossy": {
"description": "Filter option - lossy compressed audio"
},
@@ -2312,23 +2337,23 @@
"@libraryFilterFormat": {
"description": "Filter section - file format"
},
- "libraryFilterSort": "Sort",
+ "libraryFilterSort": "Sortieren",
"@libraryFilterSort": {
"description": "Filter section - sort order"
},
- "libraryFilterSortLatest": "Latest",
+ "libraryFilterSortLatest": "Neuste",
"@libraryFilterSortLatest": {
"description": "Sort option - newest first"
},
- "libraryFilterSortOldest": "Oldest",
+ "libraryFilterSortOldest": "Älteste",
"@libraryFilterSortOldest": {
"description": "Sort option - oldest first"
},
- "timeJustNow": "Just now",
+ "timeJustNow": "Gerade eben",
"@timeJustNow": {
"description": "Relative time - less than a minute ago"
},
- "timeMinutesAgo": "{count, plural, =1{1 minute ago} other{{count} minutes ago}}",
+ "timeMinutesAgo": "{count, plural, one {vor {count} Minute} other{vor {count} Minuten}}",
"@timeMinutesAgo": {
"description": "Relative time - minutes ago",
"placeholders": {
@@ -2337,7 +2362,7 @@
}
}
},
- "timeHoursAgo": "{count, plural, =1{1 hour ago} other{{count} hours ago}}",
+ "timeHoursAgo": "{count, plural, one {vor {count} Stunde} other{vor {count} Stunden}}",
"@timeHoursAgo": {
"description": "Relative time - hours ago",
"placeholders": {
@@ -2346,123 +2371,123 @@
}
}
},
- "tutorialWelcomeTitle": "Welcome to SpotiFLAC!",
+ "tutorialWelcomeTitle": "Willkommen bei SpotiFLAC!",
"@tutorialWelcomeTitle": {
"description": "Tutorial welcome page title"
},
- "tutorialWelcomeDesc": "Let's learn how to download your favorite music in lossless quality. This quick tutorial will show you the basics.",
+ "tutorialWelcomeDesc": "Lass uns lernen, wie du deine Lieblingsmusik in verlustfreier Qualität herunterlädst. Dieses schnelle Tutorial zeigt dir die Grundlagen.",
"@tutorialWelcomeDesc": {
"description": "Tutorial welcome page description"
},
- "tutorialWelcomeTip1": "Download music from Spotify, Deezer, or paste any supported URL",
+ "tutorialWelcomeTip1": "Lade Musik von Spotify, Deezer herunter oder jeden unterstützten Link einfügen",
"@tutorialWelcomeTip1": {
"description": "Tutorial welcome tip 1"
},
- "tutorialWelcomeTip2": "Get FLAC quality audio from Tidal, Qobuz, or Amazon Music",
+ "tutorialWelcomeTip2": "Hole dir FLAC Audio von Tidal, Qobuz oder Amazon Musik",
"@tutorialWelcomeTip2": {
"description": "Tutorial welcome tip 2"
},
- "tutorialWelcomeTip3": "Automatic metadata, cover art, and lyrics embedding",
+ "tutorialWelcomeTip3": "Automatische Metadaten, Cover und Lyrics einbetten",
"@tutorialWelcomeTip3": {
"description": "Tutorial welcome tip 3"
},
- "tutorialSearchTitle": "Finding Music",
+ "tutorialSearchTitle": "Suche Musik",
"@tutorialSearchTitle": {
"description": "Tutorial search page title"
},
- "tutorialSearchDesc": "There are two easy ways to find music you want to download.",
+ "tutorialSearchDesc": "Es gibt zwei einfache Möglichkeiten, Musik zu finden, die du herunterladen möchtest.",
"@tutorialSearchDesc": {
"description": "Tutorial search page description"
},
- "tutorialDownloadTitle": "Downloading Music",
+ "tutorialDownloadTitle": "Musik wird heruntergeladen",
"@tutorialDownloadTitle": {
"description": "Tutorial download page title"
},
- "tutorialDownloadDesc": "Downloading music is simple and fast. Here's how it works.",
+ "tutorialDownloadDesc": "Das Herunterladen von Musik ist einfach und schnell. So funktioniert es.",
"@tutorialDownloadDesc": {
"description": "Tutorial download page description"
},
- "tutorialLibraryTitle": "Your Library",
+ "tutorialLibraryTitle": "Deine Bibliothek",
"@tutorialLibraryTitle": {
"description": "Tutorial library page title"
},
- "tutorialLibraryDesc": "All your downloaded music is organized in the Library tab.",
+ "tutorialLibraryDesc": "Die gesamte heruntergeladene Musik ist in der Bibliothek organisiert.",
"@tutorialLibraryDesc": {
"description": "Tutorial library page description"
},
- "tutorialLibraryTip1": "View download progress and queue in the Library tab",
+ "tutorialLibraryTip1": "Fortschritt und Warteschlange im Bibliothek‑Tab anzeigen",
"@tutorialLibraryTip1": {
"description": "Tutorial library tip 1"
},
- "tutorialLibraryTip2": "Tap any track to play it with your music player",
+ "tutorialLibraryTip2": "Tippe auf einen Titel, um ihn mit deinem Musikplayer abzuspielen",
"@tutorialLibraryTip2": {
"description": "Tutorial library tip 2"
},
- "tutorialLibraryTip3": "Switch between list and grid view for better browsing",
+ "tutorialLibraryTip3": "Wechsle zwischen Listen- und Gitteransicht für ein besseres Surfen",
"@tutorialLibraryTip3": {
"description": "Tutorial library tip 3"
},
- "tutorialExtensionsTitle": "Extensions",
+ "tutorialExtensionsTitle": "Erweiterungen",
"@tutorialExtensionsTitle": {
"description": "Tutorial extensions page title"
},
- "tutorialExtensionsDesc": "Extend the app's capabilities with community extensions.",
+ "tutorialExtensionsDesc": "Erweitere die Fähigkeiten der App mit Community-Erweiterungen.",
"@tutorialExtensionsDesc": {
"description": "Tutorial extensions page description"
},
- "tutorialExtensionsTip1": "Browse the Store tab to discover useful extensions",
+ "tutorialExtensionsTip1": "Im Store Tab findest du nützliche Erweiterungen",
"@tutorialExtensionsTip1": {
"description": "Tutorial extensions tip 1"
},
- "tutorialExtensionsTip2": "Add new download providers or search sources",
+ "tutorialExtensionsTip2": "Neue Download- oder Suchanbieter hinzufügen",
"@tutorialExtensionsTip2": {
"description": "Tutorial extensions tip 2"
},
- "tutorialExtensionsTip3": "Get lyrics, enhanced metadata, and more features",
+ "tutorialExtensionsTip3": "Lyrics, erweiterte Metadaten und mehr Funktionen erhalten",
"@tutorialExtensionsTip3": {
"description": "Tutorial extensions tip 3"
},
- "tutorialSettingsTitle": "Customize Your Experience",
+ "tutorialSettingsTitle": "Passe deine Benutzererfahrung an",
"@tutorialSettingsTitle": {
"description": "Tutorial settings page title"
},
- "tutorialSettingsDesc": "Personalize the app in Settings to match your preferences.",
+ "tutorialSettingsDesc": "Personalisiere die App in den Einstellungen nach deiner Präferenz.",
"@tutorialSettingsDesc": {
"description": "Tutorial settings page description"
},
- "tutorialSettingsTip1": "Change download location and folder organization",
+ "tutorialSettingsTip1": "Downloadverzeichnis und Ordnerorganisation ändern",
"@tutorialSettingsTip1": {
"description": "Tutorial settings tip 1"
},
- "tutorialSettingsTip2": "Set default audio quality and format preferences",
+ "tutorialSettingsTip2": "Standard Audioqualität und Formateinstellungen festlegen",
"@tutorialSettingsTip2": {
"description": "Tutorial settings tip 2"
},
- "tutorialSettingsTip3": "Customize app theme and appearance",
+ "tutorialSettingsTip3": "App-Design und Aussehen anpassen",
"@tutorialSettingsTip3": {
"description": "Tutorial settings tip 3"
},
- "tutorialReadyMessage": "You're all set! Start downloading your favorite music now.",
+ "tutorialReadyMessage": "Das ist alles! Lade jetzt deine Lieblingsmusik herunter.",
"@tutorialReadyMessage": {
"description": "Tutorial completion message"
},
- "libraryForceFullScan": "Force Full Scan",
+ "libraryForceFullScan": "Vollen Neu-Scan erzwingen",
"@libraryForceFullScan": {
"description": "Button to force a complete rescan of library"
},
- "libraryForceFullScanSubtitle": "Rescan all files, ignoring cache",
+ "libraryForceFullScanSubtitle": "Alle Dateien erneut scannen und Cache ignorieren",
"@libraryForceFullScanSubtitle": {
"description": "Subtitle for force full scan button"
},
- "cleanupOrphanedDownloads": "Cleanup Orphaned Downloads",
+ "cleanupOrphanedDownloads": "Verwaiste Downloads bereinigen",
"@cleanupOrphanedDownloads": {
"description": "Button to remove history entries for deleted files"
},
- "cleanupOrphanedDownloadsSubtitle": "Remove history entries for files that no longer exist",
+ "cleanupOrphanedDownloadsSubtitle": "Verlaufseinträge für Dateien löschen, die nicht mehr existieren",
"@cleanupOrphanedDownloadsSubtitle": {
"description": "Subtitle for orphaned cleanup button"
},
- "cleanupOrphanedDownloadsResult": "Removed {count} orphaned entries from history",
+ "cleanupOrphanedDownloadsResult": "Entfernte {count} verwaiste Einträge aus dem Verlauf",
"@cleanupOrphanedDownloadsResult": {
"description": "Snackbar after orphan cleanup",
"placeholders": {
@@ -2471,23 +2496,23 @@
}
}
},
- "cleanupOrphanedDownloadsNone": "No orphaned entries found",
+ "cleanupOrphanedDownloadsNone": "Keine verwaisten Einträge gefunden",
"@cleanupOrphanedDownloadsNone": {
"description": "Snackbar when no orphans found"
},
- "cacheTitle": "Storage & Cache",
+ "cacheTitle": "Speicher & Cache",
"@cacheTitle": {
"description": "Cache management page title"
},
- "cacheSummaryTitle": "Cache overview",
+ "cacheSummaryTitle": "Cache-Übersicht",
"@cacheSummaryTitle": {
"description": "Heading for cache summary card"
},
- "cacheSummarySubtitle": "Clearing cache will not remove downloaded music files.",
+ "cacheSummarySubtitle": "Das Leeren des Caches entfernt nicht heruntergeladene Musikdateien.",
"@cacheSummarySubtitle": {
"description": "Helper text for cache summary card"
},
- "cacheEstimatedTotal": "Estimated cache usage: {size}",
+ "cacheEstimatedTotal": "Geschätzte Cache-Größe: {size}",
"@cacheEstimatedTotal": {
"description": "Total cache size shown in summary",
"placeholders": {
@@ -2496,71 +2521,71 @@
}
}
},
- "cacheSectionStorage": "Cached Data",
+ "cacheSectionStorage": "Zwischengespeicherte Daten",
"@cacheSectionStorage": {
"description": "Section header for cache entries"
},
- "cacheSectionMaintenance": "Maintenance",
+ "cacheSectionMaintenance": "Wartung",
"@cacheSectionMaintenance": {
"description": "Section header for cleanup actions"
},
- "cacheAppDirectory": "App cache directory",
+ "cacheAppDirectory": "App-Cache Verzeichnis",
"@cacheAppDirectory": {
"description": "Cache item title for app cache directory"
},
- "cacheAppDirectoryDesc": "HTTP responses, WebView data, and other temporary app data.",
+ "cacheAppDirectoryDesc": "HTTP-Antworten, WebView Daten und andere temporäre App-Daten.",
"@cacheAppDirectoryDesc": {
"description": "Description of what app cache directory contains"
},
- "cacheTempDirectory": "Temporary directory",
+ "cacheTempDirectory": "Temporäres Verzeichnis",
"@cacheTempDirectory": {
"description": "Cache item title for temporary files directory"
},
- "cacheTempDirectoryDesc": "Temporary files from downloads and audio conversion.",
+ "cacheTempDirectoryDesc": "Temporäre Dateien von Downloads und Audio-Konvertierung.",
"@cacheTempDirectoryDesc": {
"description": "Description of what temporary directory contains"
},
- "cacheCoverImage": "Cover image cache",
+ "cacheCoverImage": "Cover-Cache",
"@cacheCoverImage": {
"description": "Cache item title for persistent cover images"
},
- "cacheCoverImageDesc": "Downloaded album and track cover art. Will re-download when viewed.",
+ "cacheCoverImageDesc": "Album- und Titelcover heruntergeladen. Werden erneut heruntergeladen.",
"@cacheCoverImageDesc": {
"description": "Description of what cover image cache contains"
},
- "cacheLibraryCover": "Library cover cache",
+ "cacheLibraryCover": "Bibliotheks-Cover-Cache",
"@cacheLibraryCover": {
"description": "Cache item title for local library cover art images"
},
- "cacheLibraryCoverDesc": "Cover art extracted from local music files. Will re-extract on next scan.",
+ "cacheLibraryCoverDesc": "Cover aus lokalen Musikdateien extrahiert. Wird beim nächsten Scannen neu extrahiert.",
"@cacheLibraryCoverDesc": {
"description": "Description of what library cover cache contains"
},
- "cacheExploreFeed": "Explore feed cache",
+ "cacheExploreFeed": "Feed-Cache entdecken",
"@cacheExploreFeed": {
"description": "Cache item title for explore home feed cache"
},
- "cacheExploreFeedDesc": "Explore tab content (new releases, trending). Will refresh on next visit.",
+ "cacheExploreFeedDesc": "Startseiten-Inhalt (neue Releases, Trends). Wird bei einem Neustart aktualisiert.",
"@cacheExploreFeedDesc": {
"description": "Description of what explore feed cache contains"
},
- "cacheTrackLookup": "Track lookup cache",
+ "cacheTrackLookup": "Titel Such-Cache",
"@cacheTrackLookup": {
"description": "Cache item title for track ID lookup cache"
},
- "cacheTrackLookupDesc": "Spotify/Deezer track ID lookups. Clearing may slow next few searches.",
+ "cacheTrackLookupDesc": "Spotify/Deezer Track-ID-Lookups. Das Löschen kann die nächsten Suchergebnisse verlangsamen.",
"@cacheTrackLookupDesc": {
"description": "Description of what track lookup cache contains"
},
- "cacheCleanupUnusedDesc": "Remove orphaned download history and library entries for missing files.",
+ "cacheCleanupUnusedDesc": "Verwaisten Downloadverlauf und Bibliothekseinträge für fehlende Dateien entfernen.",
"@cacheCleanupUnusedDesc": {
"description": "Description of what cleanup unused data does"
},
- "cacheNoData": "No cached data",
+ "cacheNoData": "Keine gecachten Daten",
"@cacheNoData": {
"description": "Label when cache category has no data"
},
- "cacheSizeWithFiles": "{size} in {count} files",
+ "cacheSizeWithFiles": "{size} in {count} Dateien",
"@cacheSizeWithFiles": {
"description": "Cache size and file count",
"placeholders": {
@@ -2581,7 +2606,7 @@
}
}
},
- "cacheEntries": "{count} entries",
+ "cacheEntries": "{count} Einträge",
"@cacheEntries": {
"description": "Track cache entry count",
"placeholders": {
@@ -2590,7 +2615,7 @@
}
}
},
- "cacheClearSuccess": "Cleared: {target}",
+ "cacheClearSuccess": "Entfernt: {target}",
"@cacheClearSuccess": {
"description": "Snackbar after clearing selected cache",
"placeholders": {
@@ -2599,11 +2624,11 @@
}
}
},
- "cacheClearConfirmTitle": "Clear cache?",
+ "cacheClearConfirmTitle": "Cache leeren?",
"@cacheClearConfirmTitle": {
"description": "Dialog title before clearing one cache category"
},
- "cacheClearConfirmMessage": "This will clear cached data for {target}. Downloaded music files will not be deleted.",
+ "cacheClearConfirmMessage": "Dies löscht zwischengespeicherte Daten in {target}. Die Musikdateien werden nicht gelöscht.",
"@cacheClearConfirmMessage": {
"description": "Dialog message before clearing selected cache",
"placeholders": {
@@ -2612,27 +2637,27 @@
}
}
},
- "cacheClearAllConfirmTitle": "Clear all cache?",
+ "cacheClearAllConfirmTitle": "Gesamten Cache leeren?",
"@cacheClearAllConfirmTitle": {
"description": "Dialog title before clearing all caches"
},
- "cacheClearAllConfirmMessage": "This will clear all cache categories on this page. Downloaded music files will not be deleted.",
+ "cacheClearAllConfirmMessage": "Dadurch werden alle Cache-Kategorien auf dieser Seite gelöscht. Heruntergeladene Musikdateien werden nicht gelöscht.",
"@cacheClearAllConfirmMessage": {
"description": "Dialog message before clearing all caches"
},
- "cacheClearAll": "Clear all cache",
+ "cacheClearAll": "Gesamten Cache leeren",
"@cacheClearAll": {
"description": "Button label to clear all caches"
},
- "cacheCleanupUnused": "Cleanup unused data",
+ "cacheCleanupUnused": "Unbenutzte Daten bereinigen",
"@cacheCleanupUnused": {
"description": "Action title for cleaning unused entries"
},
- "cacheCleanupUnusedSubtitle": "Remove orphaned download history and missing library entries",
+ "cacheCleanupUnusedSubtitle": "Verwaisten Downloadverlauf und fehlende Bibliothekseinträge löschen",
"@cacheCleanupUnusedSubtitle": {
"description": "Subtitle for cleanup unused data action"
},
- "cacheCleanupResult": "Cleanup completed: {downloadCount} orphaned downloads, {libraryCount} missing library entries",
+ "cacheCleanupResult": "Bereinigung: {downloadCount} verwaiste Downloads, {libraryCount} fehlende Bibliothekseinträge",
"@cacheCleanupResult": {
"description": "Snackbar after unused data cleanup",
"placeholders": {
@@ -2644,39 +2669,39 @@
}
}
},
- "cacheRefreshStats": "Refresh stats",
+ "cacheRefreshStats": "Statistik aktualisieren",
"@cacheRefreshStats": {
"description": "Button label to refresh cache statistics"
},
- "trackSaveCoverArt": "Save Cover Art",
+ "trackSaveCoverArt": "Cover speichern",
"@trackSaveCoverArt": {
"description": "Menu action - save album cover art as file"
},
- "trackSaveCoverArtSubtitle": "Save album art as .jpg file",
+ "trackSaveCoverArtSubtitle": "Albumcover als .jpg Datei speichern",
"@trackSaveCoverArtSubtitle": {
"description": "Subtitle for save cover art action"
},
- "trackSaveLyrics": "Save Lyrics (.lrc)",
+ "trackSaveLyrics": "Lyrics als .lrc speichern",
"@trackSaveLyrics": {
"description": "Menu action - save lyrics as .lrc file"
},
- "trackSaveLyricsSubtitle": "Fetch and save lyrics as .lrc file",
+ "trackSaveLyricsSubtitle": "Lade Lyrics als .lrc Datei",
"@trackSaveLyricsSubtitle": {
"description": "Subtitle for save lyrics action"
},
- "trackSaveLyricsProgress": "Saving lyrics...",
+ "trackSaveLyricsProgress": "Speichere Lyrics...",
"@trackSaveLyricsProgress": {
"description": "Snackbar while saving lyrics to file"
},
- "trackReEnrich": "Re-enrich",
+ "trackReEnrich": "Neu-anreichern",
"@trackReEnrich": {
"description": "Menu action - re-embed metadata into audio file"
},
- "trackReEnrichOnlineSubtitle": "Search metadata online and embed into file",
+ "trackReEnrichOnlineSubtitle": "Metadaten online suchen und in Datei einbinden",
"@trackReEnrichOnlineSubtitle": {
"description": "Subtitle for re-enrich metadata action for local items"
},
- "trackEditMetadata": "Edit Metadata",
+ "trackEditMetadata": "Metadaten bearbeiten",
"@trackEditMetadata": {
"description": "Menu action - edit embedded metadata"
},
@@ -2693,7 +2718,7 @@
"@trackCoverNoSource": {
"description": "Snackbar when no cover art URL or embedded cover"
},
- "trackLyricsSaved": "Lyrics saved to {fileName}",
+ "trackLyricsSaved": "Lyrics in {fileName} gespeichert",
"@trackLyricsSaved": {
"description": "Snackbar after lyrics saved",
"placeholders": {
@@ -2702,23 +2727,23 @@
}
}
},
- "trackReEnrichProgress": "Re-enriching metadata...",
+ "trackReEnrichProgress": "Metadaten neu anreichern...",
"@trackReEnrichProgress": {
"description": "Snackbar while re-enriching metadata"
},
- "trackReEnrichSearching": "Searching metadata online...",
+ "trackReEnrichSearching": "Suche Metadaten online...",
"@trackReEnrichSearching": {
"description": "Snackbar while searching metadata from internet for local items"
},
- "trackReEnrichSuccess": "Metadata re-enriched successfully",
+ "trackReEnrichSuccess": "Metadaten erfolgreich neu angereichert",
"@trackReEnrichSuccess": {
"description": "Snackbar after successful re-enrichment"
},
- "trackReEnrichFfmpegFailed": "FFmpeg metadata embed failed",
+ "trackReEnrichFfmpegFailed": "FFmpeg Metadaten-Einbettung fehlgeschlagen",
"@trackReEnrichFfmpegFailed": {
"description": "Snackbar when FFmpeg embed fails for MP3/Opus"
},
- "trackSaveFailed": "Failed: {error}",
+ "trackSaveFailed": "Fehler: {error}",
"@trackSaveFailed": {
"description": "Snackbar when save operation fails",
"placeholders": {
@@ -2727,19 +2752,19 @@
}
}
},
- "trackConvertFormat": "Convert Format",
+ "trackConvertFormat": "Format konvertieren",
"@trackConvertFormat": {
"description": "Menu item - convert audio format"
},
- "trackConvertFormatSubtitle": "Convert to MP3 or Opus",
+ "trackConvertFormatSubtitle": "In MP3 oder Opus konvertieren",
"@trackConvertFormatSubtitle": {
"description": "Subtitle for convert format menu item"
},
- "trackConvertTitle": "Convert Audio",
+ "trackConvertTitle": "Audio konvertieren",
"@trackConvertTitle": {
"description": "Title of convert bottom sheet"
},
- "trackConvertTargetFormat": "Target Format",
+ "trackConvertTargetFormat": "Zielformat",
"@trackConvertTargetFormat": {
"description": "Label for format selection"
},
@@ -2747,11 +2772,11 @@
"@trackConvertBitrate": {
"description": "Label for bitrate selection"
},
- "trackConvertConfirmTitle": "Confirm Conversion",
+ "trackConvertConfirmTitle": "Konvertierung bestätigen",
"@trackConvertConfirmTitle": {
"description": "Confirmation dialog title"
},
- "trackConvertConfirmMessage": "Convert from {sourceFormat} to {targetFormat} at {bitrate}?\n\nThe original file will be deleted after conversion.",
+ "trackConvertConfirmMessage": "Konvertieren von {sourceFormat} in {targetFormat} bei {bitrate}?\n\nDie Originaldatei wird nach der Konvertierung gelöscht.",
"@trackConvertConfirmMessage": {
"description": "Confirmation dialog message",
"placeholders": {
@@ -2766,11 +2791,11 @@
}
}
},
- "trackConvertConverting": "Converting audio...",
+ "trackConvertConverting": "Konvertiere Audio...",
"@trackConvertConverting": {
"description": "Snackbar while converting"
},
- "trackConvertSuccess": "Converted to {format} successfully",
+ "trackConvertSuccess": "Konvertiert in {format} erfolgreich",
"@trackConvertSuccess": {
"description": "Snackbar after successful conversion",
"placeholders": {
@@ -2779,11 +2804,288 @@
}
}
},
- "trackConvertFailed": "Conversion failed",
+ "trackConvertFailed": "Konvertierung fehlgeschlagen",
"@trackConvertFailed": {
"description": "Snackbar when conversion fails"
},
- "downloadedAlbumDownloadedCount": "{count} downloaded",
+ "actionCreate": "Erstellen",
+ "@actionCreate": {
+ "description": "Generic action button - create"
+ },
+ "collectionFoldersTitle": "Meine Ordner",
+ "@collectionFoldersTitle": {
+ "description": "Library section title for custom folders"
+ },
+ "collectionWishlist": "Wunschliste",
+ "@collectionWishlist": {
+ "description": "Custom folder for saved tracks to download later"
+ },
+ "collectionLoved": "Lieblingssongs",
+ "@collectionLoved": {
+ "description": "Custom folder for favorite tracks"
+ },
+ "collectionPlaylists": "Playlisten",
+ "@collectionPlaylists": {
+ "description": "Custom user playlists folder"
+ },
+ "collectionPlaylist": "Playlist",
+ "@collectionPlaylist": {
+ "description": "Single playlist label"
+ },
+ "collectionAddToPlaylist": "Zur Playlist hinzufügen",
+ "@collectionAddToPlaylist": {
+ "description": "Action to add a track to user playlist"
+ },
+ "collectionCreatePlaylist": "Playlist erstellen",
+ "@collectionCreatePlaylist": {
+ "description": "Action to create a new playlist"
+ },
+ "collectionNoPlaylistsYet": "Noch keine Playlists",
+ "@collectionNoPlaylistsYet": {
+ "description": "Empty state title when user has no playlists"
+ },
+ "collectionNoPlaylistsSubtitle": "Playlist erstellen, um Titel zu kategorisieren",
+ "@collectionNoPlaylistsSubtitle": {
+ "description": "Empty state subtitle when user has no playlists"
+ },
+ "collectionPlaylistTracks": "{count, plural, =1{1 Titel} other{{count} Titel}}",
+ "@collectionPlaylistTracks": {
+ "description": "Track count label for custom playlists",
+ "placeholders": {
+ "count": {
+ "type": "int"
+ }
+ }
+ },
+ "collectionAddedToPlaylist": "Zu \"{playlistName} \" hinzugefügt",
+ "@collectionAddedToPlaylist": {
+ "description": "Snackbar after adding track to playlist",
+ "placeholders": {
+ "playlistName": {
+ "type": "String"
+ }
+ }
+ },
+ "collectionAlreadyInPlaylist": "Bereits in \"{playlistName}\"",
+ "@collectionAlreadyInPlaylist": {
+ "description": "Snackbar when track already exists in playlist",
+ "placeholders": {
+ "playlistName": {
+ "type": "String"
+ }
+ }
+ },
+ "collectionPlaylistCreated": "Playlist erstellt",
+ "@collectionPlaylistCreated": {
+ "description": "Snackbar after creating playlist"
+ },
+ "collectionPlaylistNameHint": "Playlist-Name",
+ "@collectionPlaylistNameHint": {
+ "description": "Hint text for playlist name input"
+ },
+ "collectionPlaylistNameRequired": "Playlist-Name ist erforderlich",
+ "@collectionPlaylistNameRequired": {
+ "description": "Validation error for empty playlist name"
+ },
+ "collectionRenamePlaylist": "Playlist umbenennen",
+ "@collectionRenamePlaylist": {
+ "description": "Action to rename playlist"
+ },
+ "collectionDeletePlaylist": "Playlist löschen",
+ "@collectionDeletePlaylist": {
+ "description": "Action to delete playlist"
+ },
+ "collectionDeletePlaylistMessage": "Willst du \"{playlistName}\" und alle darin enthaltenen Titel löschen?",
+ "@collectionDeletePlaylistMessage": {
+ "description": "Confirmation message for deleting playlist",
+ "placeholders": {
+ "playlistName": {
+ "type": "String"
+ }
+ }
+ },
+ "collectionPlaylistDeleted": "Playlist gelöscht",
+ "@collectionPlaylistDeleted": {
+ "description": "Snackbar after deleting playlist"
+ },
+ "collectionPlaylistRenamed": "Playlist umbenannt",
+ "@collectionPlaylistRenamed": {
+ "description": "Snackbar after renaming playlist"
+ },
+ "collectionWishlistEmptyTitle": "Wunschliste ist leer",
+ "@collectionWishlistEmptyTitle": {
+ "description": "Wishlist empty state title"
+ },
+ "collectionWishlistEmptySubtitle": "Tippe auf das + bei den Titeln, um sie zum späteren Herunterladen zu speichern",
+ "@collectionWishlistEmptySubtitle": {
+ "description": "Wishlist empty state subtitle"
+ },
+ "collectionLovedEmptyTitle": "Lieblingssongs sind leer",
+ "@collectionLovedEmptyTitle": {
+ "description": "Loved empty state title"
+ },
+ "collectionLovedEmptySubtitle": "Tippe auf das Herz, um deine Favoriten zu behalten",
+ "@collectionLovedEmptySubtitle": {
+ "description": "Loved empty state subtitle"
+ },
+ "collectionPlaylistEmptyTitle": "Die Playlist ist leer",
+ "@collectionPlaylistEmptyTitle": {
+ "description": "Playlist empty state title"
+ },
+ "collectionPlaylistEmptySubtitle": "Drücke lange + auf einem beliebigen Titel, um ihn hier hinzuzufügen",
+ "@collectionPlaylistEmptySubtitle": {
+ "description": "Playlist empty state subtitle"
+ },
+ "collectionRemoveFromPlaylist": "Von Playlist entfernen",
+ "@collectionRemoveFromPlaylist": {
+ "description": "Tooltip for removing track from playlist"
+ },
+ "collectionRemoveFromFolder": "Aus Ordner entfernen",
+ "@collectionRemoveFromFolder": {
+ "description": "Tooltip for removing track from wishlist/loved folder"
+ },
+ "collectionRemoved": "\"{trackName}\" entfernt",
+ "@collectionRemoved": {
+ "description": "Snackbar after removing a track from a collection",
+ "placeholders": {
+ "trackName": {
+ "type": "String"
+ }
+ }
+ },
+ "collectionAddedToLoved": "\"{trackName}\" zu Lieblingssongs hinzugefügt",
+ "@collectionAddedToLoved": {
+ "description": "Snackbar after adding track to loved folder",
+ "placeholders": {
+ "trackName": {
+ "type": "String"
+ }
+ }
+ },
+ "collectionRemovedFromLoved": "\"{trackName}\" aus Lieblingssongs entfernt",
+ "@collectionRemovedFromLoved": {
+ "description": "Snackbar after removing track from loved folder",
+ "placeholders": {
+ "trackName": {
+ "type": "String"
+ }
+ }
+ },
+ "collectionAddedToWishlist": "\"{trackName}\" zur Wunschliste hinzugefügt",
+ "@collectionAddedToWishlist": {
+ "description": "Snackbar after adding track to wishlist",
+ "placeholders": {
+ "trackName": {
+ "type": "String"
+ }
+ }
+ },
+ "collectionRemovedFromWishlist": "\"{trackName}\" aus der Wunschliste entfernt",
+ "@collectionRemovedFromWishlist": {
+ "description": "Snackbar after removing track from wishlist",
+ "placeholders": {
+ "trackName": {
+ "type": "String"
+ }
+ }
+ },
+ "trackOptionAddToLoved": "Zu Lieblingssongs hinzufügen",
+ "@trackOptionAddToLoved": {
+ "description": "Bottom sheet action label - add track to loved folder"
+ },
+ "trackOptionRemoveFromLoved": "Aus Lieblingssongs entfernt",
+ "@trackOptionRemoveFromLoved": {
+ "description": "Bottom sheet action label - remove track from loved folder"
+ },
+ "trackOptionAddToWishlist": "Zur Wunschliste hinzufügen",
+ "@trackOptionAddToWishlist": {
+ "description": "Bottom sheet action label - add track to wishlist"
+ },
+ "trackOptionRemoveFromWishlist": "Von der Wunschliste entfernen",
+ "@trackOptionRemoveFromWishlist": {
+ "description": "Bottom sheet action label - remove track from wishlist"
+ },
+ "collectionPlaylistChangeCover": "Coverbild ändern",
+ "@collectionPlaylistChangeCover": {
+ "description": "Bottom sheet action to pick a custom cover image for a playlist"
+ },
+ "collectionPlaylistRemoveCover": "Cover entfernen",
+ "@collectionPlaylistRemoveCover": {
+ "description": "Bottom sheet action to remove custom cover image from a playlist"
+ },
+ "selectionShareCount": "Teile {count} {count, plural, one {Titel} other{Titel}}",
+ "@selectionShareCount": {
+ "description": "Share button text with count in selection mode",
+ "placeholders": {
+ "count": {
+ "type": "int"
+ }
+ }
+ },
+ "selectionShareNoFiles": "Keine teilbare Dateien gefunden",
+ "@selectionShareNoFiles": {
+ "description": "Snackbar when no selected files exist on disk"
+ },
+ "selectionConvertCount": "Konvertiere {count} {count, plural, one {Titel} other{Titel}}",
+ "@selectionConvertCount": {
+ "description": "Convert button text with count in selection mode",
+ "placeholders": {
+ "count": {
+ "type": "int"
+ }
+ }
+ },
+ "selectionConvertNoConvertible": "Keine konvertierbare Titel ausgewählt",
+ "@selectionConvertNoConvertible": {
+ "description": "Snackbar when no selected tracks support conversion"
+ },
+ "selectionBatchConvertConfirmTitle": "Batch-Konvertierung",
+ "@selectionBatchConvertConfirmTitle": {
+ "description": "Confirmation dialog title for batch conversion"
+ },
+ "selectionBatchConvertConfirmMessage": "Konvertiere {count} {format} {count, plural, one {Titel} other{Titel}} zu {bitrate}?\n\nOriginaldateien werden nach der Konvertierung gelöscht.",
+ "@selectionBatchConvertConfirmMessage": {
+ "description": "Confirmation dialog message for batch conversion",
+ "placeholders": {
+ "count": {
+ "type": "int"
+ },
+ "format": {
+ "type": "String"
+ },
+ "bitrate": {
+ "type": "String"
+ }
+ }
+ },
+ "selectionBatchConvertProgress": "Konvertiere {current} von {total}...",
+ "@selectionBatchConvertProgress": {
+ "description": "Snackbar during batch conversion progress",
+ "placeholders": {
+ "current": {
+ "type": "int"
+ },
+ "total": {
+ "type": "int"
+ }
+ }
+ },
+ "selectionBatchConvertSuccess": "{success} von {total} Titeln in {format} konvertiert",
+ "@selectionBatchConvertSuccess": {
+ "description": "Snackbar after batch conversion completes",
+ "placeholders": {
+ "success": {
+ "type": "int"
+ },
+ "total": {
+ "type": "int"
+ },
+ "format": {
+ "type": "String"
+ }
+ }
+ },
+ "downloadedAlbumDownloadedCount": "{count} heruntergeladen",
"@downloadedAlbumDownloadedCount": {
"description": "Downloaded tracks count badge",
"placeholders": {
@@ -2792,7 +3094,7 @@
}
}
},
- "downloadUseAlbumArtistForFoldersAlbumSubtitle": "Artist folders use Album Artist when available",
+ "downloadUseAlbumArtistForFoldersAlbumSubtitle": "Künstlerordner verwenden den Album-Interpreten, wenn verfügbar",
"@downloadUseAlbumArtistForFoldersAlbumSubtitle": {
"description": "Subtitle when Album Artist is used for folder naming"
},
@@ -2800,4 +3102,4 @@
"@downloadUseAlbumArtistForFoldersTrackSubtitle": {
"description": "Subtitle when Track Artist is used for folder naming"
}
-}
+}
\ No newline at end of file
diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb
index ca416fa0..03313d7b 100644
--- a/lib/l10n/arb/app_en.arb
+++ b/lib/l10n/arb/app_en.arb
@@ -450,7 +450,7 @@
"@aboutSpotiSaverDesc": {
"description": "Credit for SpotiSaver API"
},
- "aboutAppDescription": "Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.",
+ "aboutAppDescription": "Download Spotify tracks in lossless quality from Tidal and Qobuz.",
"@aboutAppDescription": {
"description": "App description in header card"
},
@@ -897,6 +897,18 @@
"@errorNoTracksFound": {
"description": "Error - search returned no results"
},
+ "errorUrlNotRecognized": "Link not recognized",
+ "@errorUrlNotRecognized": {
+ "description": "Error title - URL not handled by any extension or service"
+ },
+ "errorUrlNotRecognizedMessage": "This link is not supported. Make sure the URL is correct and a compatible extension is installed.",
+ "@errorUrlNotRecognizedMessage": {
+ "description": "Error message - URL not recognized explanation"
+ },
+ "errorUrlFetchFailed": "Failed to load content from this link. Please try again.",
+ "@errorUrlFetchFailed": {
+ "description": "Error message - generic URL fetch failure"
+ },
"errorMissingExtensionSource": "Cannot load {item}: missing extension source",
"@errorMissingExtensionSource": {
"description": "Error - extension source not available",
@@ -1003,6 +1015,14 @@
"@folderOrganizationNone": {
"description": "Folder option - flat structure"
},
+ "folderOrganizationByPlaylist": "By Playlist",
+ "@folderOrganizationByPlaylist": {
+ "description": "Folder option - playlist folders"
+ },
+ "folderOrganizationByPlaylistSubtitle": "Separate folder for each playlist",
+ "@folderOrganizationByPlaylistSubtitle": {
+ "description": "Subtitle for playlist folder option"
+ },
"folderOrganizationByArtist": "By Artist",
"@folderOrganizationByArtist": {
"description": "Folder option - artist folders"
@@ -1097,7 +1117,7 @@
},
"providerBuiltIn": "Built-in",
"@providerBuiltIn": {
- "description": "Label for built-in providers (Tidal/Qobuz/Amazon)"
+ "description": "Label for built-in providers (Tidal/Qobuz)"
},
"providerExtension": "Extension",
"@providerExtension": {
@@ -2383,7 +2403,7 @@
"@tutorialWelcomeTip1": {
"description": "Tutorial welcome tip 1"
},
- "tutorialWelcomeTip2": "Get FLAC quality audio from Tidal, Qobuz, or Amazon Music",
+ "tutorialWelcomeTip2": "Get FLAC quality audio from Tidal, Qobuz, or Deezer",
"@tutorialWelcomeTip2": {
"description": "Tutorial welcome tip 2"
},
@@ -2808,6 +2828,90 @@
"@trackConvertFailed": {
"description": "Snackbar when conversion fails"
},
+ "cueSplitTitle": "Split CUE Sheet",
+ "@cueSplitTitle": {
+ "description": "Title for CUE split bottom sheet"
+ },
+ "cueSplitSubtitle": "Split CUE+FLAC into individual tracks",
+ "@cueSplitSubtitle": {
+ "description": "Subtitle for CUE split menu item"
+ },
+ "cueSplitAlbum": "Album: {album}",
+ "@cueSplitAlbum": {
+ "description": "Album name in CUE split sheet",
+ "placeholders": {
+ "album": {
+ "type": "String"
+ }
+ }
+ },
+ "cueSplitArtist": "Artist: {artist}",
+ "@cueSplitArtist": {
+ "description": "Artist name in CUE split sheet",
+ "placeholders": {
+ "artist": {
+ "type": "String"
+ }
+ }
+ },
+ "cueSplitTrackCount": "{count} tracks",
+ "@cueSplitTrackCount": {
+ "description": "Number of tracks in CUE sheet",
+ "placeholders": {
+ "count": {
+ "type": "int"
+ }
+ }
+ },
+ "cueSplitConfirmTitle": "Split CUE Album",
+ "@cueSplitConfirmTitle": {
+ "description": "CUE split confirmation dialog title"
+ },
+ "cueSplitConfirmMessage": "Split \"{album}\" into {count} individual FLAC files?\n\nFiles will be saved to the same directory.",
+ "@cueSplitConfirmMessage": {
+ "description": "CUE split confirmation dialog message",
+ "placeholders": {
+ "album": {
+ "type": "String"
+ },
+ "count": {
+ "type": "int"
+ }
+ }
+ },
+ "cueSplitSplitting": "Splitting CUE sheet... ({current}/{total})",
+ "@cueSplitSplitting": {
+ "description": "Snackbar while splitting CUE",
+ "placeholders": {
+ "current": {
+ "type": "int"
+ },
+ "total": {
+ "type": "int"
+ }
+ }
+ },
+ "cueSplitSuccess": "Split into {count} tracks successfully",
+ "@cueSplitSuccess": {
+ "description": "Snackbar after successful CUE split",
+ "placeholders": {
+ "count": {
+ "type": "int"
+ }
+ }
+ },
+ "cueSplitFailed": "CUE split failed",
+ "@cueSplitFailed": {
+ "description": "Snackbar when CUE split fails"
+ },
+ "cueSplitNoAudioFile": "Audio file not found for this CUE sheet",
+ "@cueSplitNoAudioFile": {
+ "description": "Error when CUE audio file is missing"
+ },
+ "cueSplitButton": "Split into Tracks",
+ "@cueSplitButton": {
+ "description": "Button text to start CUE splitting"
+ },
"actionCreate": "Create",
"@actionCreate": {
"description": "Generic action button - create"
diff --git a/lib/l10n/arb/app_es.arb b/lib/l10n/arb/app_es.arb
index 497c11bc..45733fc8 100644
--- a/lib/l10n/arb/app_es.arb
+++ b/lib/l10n/arb/app_es.arb
@@ -402,7 +402,7 @@
"@aboutDabMusicDesc": {
"description": "Credit for DAB Music API"
},
- "aboutAppDescription": "Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.",
+ "aboutAppDescription": "Download Spotify tracks in lossless quality from Tidal and Qobuz.",
"@aboutAppDescription": {
"description": "App description in header card"
},
@@ -1005,7 +1005,7 @@
},
"providerBuiltIn": "Built-in",
"@providerBuiltIn": {
- "description": "Label for built-in providers (Tidal/Qobuz/Amazon)"
+ "description": "Label for built-in providers (Tidal/Qobuz)"
},
"providerExtension": "Extension",
"@providerExtension": {
diff --git a/lib/l10n/arb/app_es_ES.arb b/lib/l10n/arb/app_es_ES.arb
index 304b9cd8..f7d481c3 100644
--- a/lib/l10n/arb/app_es_ES.arb
+++ b/lib/l10n/arb/app_es_ES.arb
@@ -450,7 +450,7 @@
"@aboutSpotiSaverDesc": {
"description": "Credit for SpotiSaver API"
},
- "aboutAppDescription": "Descarga pistas de Spotify con calidad sin pérdida de Tidal, Qobuz y Amazon Music.",
+ "aboutAppDescription": "Descarga pistas de Spotify con calidad sin pérdida de Tidal y Qobuz.",
"@aboutAppDescription": {
"description": "App description in header card"
},
@@ -1089,7 +1089,7 @@
},
"providerBuiltIn": "Integrado",
"@providerBuiltIn": {
- "description": "Label for built-in providers (Tidal/Qobuz/Amazon)"
+ "description": "Label for built-in providers (Tidal/Qobuz)"
},
"providerExtension": "Extensión",
"@providerExtension": {
@@ -2358,7 +2358,7 @@
"@tutorialWelcomeTip1": {
"description": "Tutorial welcome tip 1"
},
- "tutorialWelcomeTip2": "Get FLAC quality audio from Tidal, Qobuz, or Amazon Music",
+ "tutorialWelcomeTip2": "Obtén audio en calidad FLAC de Tidal, Qobuz o Deezer",
"@tutorialWelcomeTip2": {
"description": "Tutorial welcome tip 2"
},
diff --git a/lib/l10n/arb/app_fr.arb b/lib/l10n/arb/app_fr.arb
index f7e9c2d1..3ddb731a 100644
--- a/lib/l10n/arb/app_fr.arb
+++ b/lib/l10n/arb/app_fr.arb
@@ -991,6 +991,14 @@
"@filenameFormat": {
"description": "Setting title - filename pattern"
},
+ "filenameShowAdvancedTags": "Show advanced tags",
+ "@filenameShowAdvancedTags": {
+ "description": "Toggle label for showing advanced filename tags"
+ },
+ "filenameShowAdvancedTagsDescription": "Enable formatted tags for track padding and date patterns",
+ "@filenameShowAdvancedTagsDescription": {
+ "description": "Description for advanced filename tag toggle"
+ },
"folderOrganizationNone": "No organization",
"@folderOrganizationNone": {
"description": "Folder option - flat structure"
@@ -1749,6 +1757,14 @@
"@youtubeQualityNote": {
"description": "Note for YouTube service explaining lossy-only quality"
},
+ "youtubeOpusBitrateTitle": "YouTube Opus Bitrate",
+ "@youtubeOpusBitrateTitle": {
+ "description": "Title for YouTube Opus bitrate setting"
+ },
+ "youtubeMp3BitrateTitle": "YouTube MP3 Bitrate",
+ "@youtubeMp3BitrateTitle": {
+ "description": "Title for YouTube MP3 bitrate setting"
+ },
"downloadAskBeforeDownload": "Ask Before Download",
"@downloadAskBeforeDownload": {
"description": "Setting - show quality picker"
@@ -2198,6 +2214,15 @@
"@libraryAboutDescription": {
"description": "Description of local library feature"
},
+ "libraryTracksUnit": "{count, plural, =1{track} other{tracks}}",
+ "@libraryTracksUnit": {
+ "description": "Unit label for tracks count (without the number itself)",
+ "placeholders": {
+ "count": {
+ "type": "int"
+ }
+ }
+ },
"libraryLastScanned": "Last scanned: {time}",
"@libraryLastScanned": {
"description": "Last scan time display",
@@ -2783,6 +2808,283 @@
"@trackConvertFailed": {
"description": "Snackbar when conversion fails"
},
+ "actionCreate": "Create",
+ "@actionCreate": {
+ "description": "Generic action button - create"
+ },
+ "collectionFoldersTitle": "My folders",
+ "@collectionFoldersTitle": {
+ "description": "Library section title for custom folders"
+ },
+ "collectionWishlist": "Wishlist",
+ "@collectionWishlist": {
+ "description": "Custom folder for saved tracks to download later"
+ },
+ "collectionLoved": "Loved",
+ "@collectionLoved": {
+ "description": "Custom folder for favorite tracks"
+ },
+ "collectionPlaylists": "Playlists",
+ "@collectionPlaylists": {
+ "description": "Custom user playlists folder"
+ },
+ "collectionPlaylist": "Playlist",
+ "@collectionPlaylist": {
+ "description": "Single playlist label"
+ },
+ "collectionAddToPlaylist": "Add to playlist",
+ "@collectionAddToPlaylist": {
+ "description": "Action to add a track to user playlist"
+ },
+ "collectionCreatePlaylist": "Create playlist",
+ "@collectionCreatePlaylist": {
+ "description": "Action to create a new playlist"
+ },
+ "collectionNoPlaylistsYet": "No playlists yet",
+ "@collectionNoPlaylistsYet": {
+ "description": "Empty state title when user has no playlists"
+ },
+ "collectionNoPlaylistsSubtitle": "Create a playlist to start categorizing tracks",
+ "@collectionNoPlaylistsSubtitle": {
+ "description": "Empty state subtitle when user has no playlists"
+ },
+ "collectionPlaylistTracks": "{count, plural, =1{1 track} other{{count} tracks}}",
+ "@collectionPlaylistTracks": {
+ "description": "Track count label for custom playlists",
+ "placeholders": {
+ "count": {
+ "type": "int"
+ }
+ }
+ },
+ "collectionAddedToPlaylist": "Added to \"{playlistName}\"",
+ "@collectionAddedToPlaylist": {
+ "description": "Snackbar after adding track to playlist",
+ "placeholders": {
+ "playlistName": {
+ "type": "String"
+ }
+ }
+ },
+ "collectionAlreadyInPlaylist": "Already in \"{playlistName}\"",
+ "@collectionAlreadyInPlaylist": {
+ "description": "Snackbar when track already exists in playlist",
+ "placeholders": {
+ "playlistName": {
+ "type": "String"
+ }
+ }
+ },
+ "collectionPlaylistCreated": "Playlist created",
+ "@collectionPlaylistCreated": {
+ "description": "Snackbar after creating playlist"
+ },
+ "collectionPlaylistNameHint": "Playlist name",
+ "@collectionPlaylistNameHint": {
+ "description": "Hint text for playlist name input"
+ },
+ "collectionPlaylistNameRequired": "Playlist name is required",
+ "@collectionPlaylistNameRequired": {
+ "description": "Validation error for empty playlist name"
+ },
+ "collectionRenamePlaylist": "Rename playlist",
+ "@collectionRenamePlaylist": {
+ "description": "Action to rename playlist"
+ },
+ "collectionDeletePlaylist": "Delete playlist",
+ "@collectionDeletePlaylist": {
+ "description": "Action to delete playlist"
+ },
+ "collectionDeletePlaylistMessage": "Delete \"{playlistName}\" and all tracks inside it?",
+ "@collectionDeletePlaylistMessage": {
+ "description": "Confirmation message for deleting playlist",
+ "placeholders": {
+ "playlistName": {
+ "type": "String"
+ }
+ }
+ },
+ "collectionPlaylistDeleted": "Playlist deleted",
+ "@collectionPlaylistDeleted": {
+ "description": "Snackbar after deleting playlist"
+ },
+ "collectionPlaylistRenamed": "Playlist renamed",
+ "@collectionPlaylistRenamed": {
+ "description": "Snackbar after renaming playlist"
+ },
+ "collectionWishlistEmptyTitle": "Wishlist is empty",
+ "@collectionWishlistEmptyTitle": {
+ "description": "Wishlist empty state title"
+ },
+ "collectionWishlistEmptySubtitle": "Tap + on tracks to save what you want to download later",
+ "@collectionWishlistEmptySubtitle": {
+ "description": "Wishlist empty state subtitle"
+ },
+ "collectionLovedEmptyTitle": "Loved folder is empty",
+ "@collectionLovedEmptyTitle": {
+ "description": "Loved empty state title"
+ },
+ "collectionLovedEmptySubtitle": "Tap love on tracks to keep your favorites",
+ "@collectionLovedEmptySubtitle": {
+ "description": "Loved empty state subtitle"
+ },
+ "collectionPlaylistEmptyTitle": "Playlist is empty",
+ "@collectionPlaylistEmptyTitle": {
+ "description": "Playlist empty state title"
+ },
+ "collectionPlaylistEmptySubtitle": "Long-press + on any track to add it here",
+ "@collectionPlaylistEmptySubtitle": {
+ "description": "Playlist empty state subtitle"
+ },
+ "collectionRemoveFromPlaylist": "Remove from playlist",
+ "@collectionRemoveFromPlaylist": {
+ "description": "Tooltip for removing track from playlist"
+ },
+ "collectionRemoveFromFolder": "Remove from folder",
+ "@collectionRemoveFromFolder": {
+ "description": "Tooltip for removing track from wishlist/loved folder"
+ },
+ "collectionRemoved": "\"{trackName}\" removed",
+ "@collectionRemoved": {
+ "description": "Snackbar after removing a track from a collection",
+ "placeholders": {
+ "trackName": {
+ "type": "String"
+ }
+ }
+ },
+ "collectionAddedToLoved": "\"{trackName}\" added to Loved",
+ "@collectionAddedToLoved": {
+ "description": "Snackbar after adding track to loved folder",
+ "placeholders": {
+ "trackName": {
+ "type": "String"
+ }
+ }
+ },
+ "collectionRemovedFromLoved": "\"{trackName}\" removed from Loved",
+ "@collectionRemovedFromLoved": {
+ "description": "Snackbar after removing track from loved folder",
+ "placeholders": {
+ "trackName": {
+ "type": "String"
+ }
+ }
+ },
+ "collectionAddedToWishlist": "\"{trackName}\" added to Wishlist",
+ "@collectionAddedToWishlist": {
+ "description": "Snackbar after adding track to wishlist",
+ "placeholders": {
+ "trackName": {
+ "type": "String"
+ }
+ }
+ },
+ "collectionRemovedFromWishlist": "\"{trackName}\" removed from Wishlist",
+ "@collectionRemovedFromWishlist": {
+ "description": "Snackbar after removing track from wishlist",
+ "placeholders": {
+ "trackName": {
+ "type": "String"
+ }
+ }
+ },
+ "trackOptionAddToLoved": "Add to Loved",
+ "@trackOptionAddToLoved": {
+ "description": "Bottom sheet action label - add track to loved folder"
+ },
+ "trackOptionRemoveFromLoved": "Remove from Loved",
+ "@trackOptionRemoveFromLoved": {
+ "description": "Bottom sheet action label - remove track from loved folder"
+ },
+ "trackOptionAddToWishlist": "Add to Wishlist",
+ "@trackOptionAddToWishlist": {
+ "description": "Bottom sheet action label - add track to wishlist"
+ },
+ "trackOptionRemoveFromWishlist": "Remove from Wishlist",
+ "@trackOptionRemoveFromWishlist": {
+ "description": "Bottom sheet action label - remove track from wishlist"
+ },
+ "collectionPlaylistChangeCover": "Change cover image",
+ "@collectionPlaylistChangeCover": {
+ "description": "Bottom sheet action to pick a custom cover image for a playlist"
+ },
+ "collectionPlaylistRemoveCover": "Remove cover image",
+ "@collectionPlaylistRemoveCover": {
+ "description": "Bottom sheet action to remove custom cover image from a playlist"
+ },
+ "selectionShareCount": "Share {count} {count, plural, =1{track} other{tracks}}",
+ "@selectionShareCount": {
+ "description": "Share button text with count in selection mode",
+ "placeholders": {
+ "count": {
+ "type": "int"
+ }
+ }
+ },
+ "selectionShareNoFiles": "No shareable files found",
+ "@selectionShareNoFiles": {
+ "description": "Snackbar when no selected files exist on disk"
+ },
+ "selectionConvertCount": "Convert {count} {count, plural, =1{track} other{tracks}}",
+ "@selectionConvertCount": {
+ "description": "Convert button text with count in selection mode",
+ "placeholders": {
+ "count": {
+ "type": "int"
+ }
+ }
+ },
+ "selectionConvertNoConvertible": "No convertible tracks selected",
+ "@selectionConvertNoConvertible": {
+ "description": "Snackbar when no selected tracks support conversion"
+ },
+ "selectionBatchConvertConfirmTitle": "Batch Convert",
+ "@selectionBatchConvertConfirmTitle": {
+ "description": "Confirmation dialog title for batch conversion"
+ },
+ "selectionBatchConvertConfirmMessage": "Convert {count} {count, plural, =1{track} other{tracks}} to {format} at {bitrate}?\n\nOriginal files will be deleted after conversion.",
+ "@selectionBatchConvertConfirmMessage": {
+ "description": "Confirmation dialog message for batch conversion",
+ "placeholders": {
+ "count": {
+ "type": "int"
+ },
+ "format": {
+ "type": "String"
+ },
+ "bitrate": {
+ "type": "String"
+ }
+ }
+ },
+ "selectionBatchConvertProgress": "Converting {current} of {total}...",
+ "@selectionBatchConvertProgress": {
+ "description": "Snackbar during batch conversion progress",
+ "placeholders": {
+ "current": {
+ "type": "int"
+ },
+ "total": {
+ "type": "int"
+ }
+ }
+ },
+ "selectionBatchConvertSuccess": "Converted {success} of {total} tracks to {format}",
+ "@selectionBatchConvertSuccess": {
+ "description": "Snackbar after batch conversion completes",
+ "placeholders": {
+ "success": {
+ "type": "int"
+ },
+ "total": {
+ "type": "int"
+ },
+ "format": {
+ "type": "String"
+ }
+ }
+ },
"downloadedAlbumDownloadedCount": "{count} downloaded",
"@downloadedAlbumDownloadedCount": {
"description": "Downloaded tracks count badge",
@@ -2800,4 +3102,4 @@
"@downloadUseAlbumArtistForFoldersTrackSubtitle": {
"description": "Subtitle when Track Artist is used for folder naming"
}
-}
+}
\ No newline at end of file
diff --git a/lib/l10n/arb/app_hi.arb b/lib/l10n/arb/app_hi.arb
index 064202f2..33040826 100644
--- a/lib/l10n/arb/app_hi.arb
+++ b/lib/l10n/arb/app_hi.arb
@@ -991,6 +991,14 @@
"@filenameFormat": {
"description": "Setting title - filename pattern"
},
+ "filenameShowAdvancedTags": "Show advanced tags",
+ "@filenameShowAdvancedTags": {
+ "description": "Toggle label for showing advanced filename tags"
+ },
+ "filenameShowAdvancedTagsDescription": "Enable formatted tags for track padding and date patterns",
+ "@filenameShowAdvancedTagsDescription": {
+ "description": "Description for advanced filename tag toggle"
+ },
"folderOrganizationNone": "No organization",
"@folderOrganizationNone": {
"description": "Folder option - flat structure"
@@ -1749,6 +1757,14 @@
"@youtubeQualityNote": {
"description": "Note for YouTube service explaining lossy-only quality"
},
+ "youtubeOpusBitrateTitle": "YouTube Opus Bitrate",
+ "@youtubeOpusBitrateTitle": {
+ "description": "Title for YouTube Opus bitrate setting"
+ },
+ "youtubeMp3BitrateTitle": "YouTube MP3 Bitrate",
+ "@youtubeMp3BitrateTitle": {
+ "description": "Title for YouTube MP3 bitrate setting"
+ },
"downloadAskBeforeDownload": "Ask Before Download",
"@downloadAskBeforeDownload": {
"description": "Setting - show quality picker"
@@ -2198,6 +2214,15 @@
"@libraryAboutDescription": {
"description": "Description of local library feature"
},
+ "libraryTracksUnit": "{count, plural, =1{track} other{tracks}}",
+ "@libraryTracksUnit": {
+ "description": "Unit label for tracks count (without the number itself)",
+ "placeholders": {
+ "count": {
+ "type": "int"
+ }
+ }
+ },
"libraryLastScanned": "Last scanned: {time}",
"@libraryLastScanned": {
"description": "Last scan time display",
@@ -2783,6 +2808,283 @@
"@trackConvertFailed": {
"description": "Snackbar when conversion fails"
},
+ "actionCreate": "Create",
+ "@actionCreate": {
+ "description": "Generic action button - create"
+ },
+ "collectionFoldersTitle": "My folders",
+ "@collectionFoldersTitle": {
+ "description": "Library section title for custom folders"
+ },
+ "collectionWishlist": "Wishlist",
+ "@collectionWishlist": {
+ "description": "Custom folder for saved tracks to download later"
+ },
+ "collectionLoved": "Loved",
+ "@collectionLoved": {
+ "description": "Custom folder for favorite tracks"
+ },
+ "collectionPlaylists": "Playlists",
+ "@collectionPlaylists": {
+ "description": "Custom user playlists folder"
+ },
+ "collectionPlaylist": "Playlist",
+ "@collectionPlaylist": {
+ "description": "Single playlist label"
+ },
+ "collectionAddToPlaylist": "Add to playlist",
+ "@collectionAddToPlaylist": {
+ "description": "Action to add a track to user playlist"
+ },
+ "collectionCreatePlaylist": "Create playlist",
+ "@collectionCreatePlaylist": {
+ "description": "Action to create a new playlist"
+ },
+ "collectionNoPlaylistsYet": "No playlists yet",
+ "@collectionNoPlaylistsYet": {
+ "description": "Empty state title when user has no playlists"
+ },
+ "collectionNoPlaylistsSubtitle": "Create a playlist to start categorizing tracks",
+ "@collectionNoPlaylistsSubtitle": {
+ "description": "Empty state subtitle when user has no playlists"
+ },
+ "collectionPlaylistTracks": "{count, plural, =1{1 track} other{{count} tracks}}",
+ "@collectionPlaylistTracks": {
+ "description": "Track count label for custom playlists",
+ "placeholders": {
+ "count": {
+ "type": "int"
+ }
+ }
+ },
+ "collectionAddedToPlaylist": "Added to \"{playlistName}\"",
+ "@collectionAddedToPlaylist": {
+ "description": "Snackbar after adding track to playlist",
+ "placeholders": {
+ "playlistName": {
+ "type": "String"
+ }
+ }
+ },
+ "collectionAlreadyInPlaylist": "Already in \"{playlistName}\"",
+ "@collectionAlreadyInPlaylist": {
+ "description": "Snackbar when track already exists in playlist",
+ "placeholders": {
+ "playlistName": {
+ "type": "String"
+ }
+ }
+ },
+ "collectionPlaylistCreated": "Playlist created",
+ "@collectionPlaylistCreated": {
+ "description": "Snackbar after creating playlist"
+ },
+ "collectionPlaylistNameHint": "Playlist name",
+ "@collectionPlaylistNameHint": {
+ "description": "Hint text for playlist name input"
+ },
+ "collectionPlaylistNameRequired": "Playlist name is required",
+ "@collectionPlaylistNameRequired": {
+ "description": "Validation error for empty playlist name"
+ },
+ "collectionRenamePlaylist": "Rename playlist",
+ "@collectionRenamePlaylist": {
+ "description": "Action to rename playlist"
+ },
+ "collectionDeletePlaylist": "Delete playlist",
+ "@collectionDeletePlaylist": {
+ "description": "Action to delete playlist"
+ },
+ "collectionDeletePlaylistMessage": "Delete \"{playlistName}\" and all tracks inside it?",
+ "@collectionDeletePlaylistMessage": {
+ "description": "Confirmation message for deleting playlist",
+ "placeholders": {
+ "playlistName": {
+ "type": "String"
+ }
+ }
+ },
+ "collectionPlaylistDeleted": "Playlist deleted",
+ "@collectionPlaylistDeleted": {
+ "description": "Snackbar after deleting playlist"
+ },
+ "collectionPlaylistRenamed": "Playlist renamed",
+ "@collectionPlaylistRenamed": {
+ "description": "Snackbar after renaming playlist"
+ },
+ "collectionWishlistEmptyTitle": "Wishlist is empty",
+ "@collectionWishlistEmptyTitle": {
+ "description": "Wishlist empty state title"
+ },
+ "collectionWishlistEmptySubtitle": "Tap + on tracks to save what you want to download later",
+ "@collectionWishlistEmptySubtitle": {
+ "description": "Wishlist empty state subtitle"
+ },
+ "collectionLovedEmptyTitle": "Loved folder is empty",
+ "@collectionLovedEmptyTitle": {
+ "description": "Loved empty state title"
+ },
+ "collectionLovedEmptySubtitle": "Tap love on tracks to keep your favorites",
+ "@collectionLovedEmptySubtitle": {
+ "description": "Loved empty state subtitle"
+ },
+ "collectionPlaylistEmptyTitle": "Playlist is empty",
+ "@collectionPlaylistEmptyTitle": {
+ "description": "Playlist empty state title"
+ },
+ "collectionPlaylistEmptySubtitle": "Long-press + on any track to add it here",
+ "@collectionPlaylistEmptySubtitle": {
+ "description": "Playlist empty state subtitle"
+ },
+ "collectionRemoveFromPlaylist": "Remove from playlist",
+ "@collectionRemoveFromPlaylist": {
+ "description": "Tooltip for removing track from playlist"
+ },
+ "collectionRemoveFromFolder": "Remove from folder",
+ "@collectionRemoveFromFolder": {
+ "description": "Tooltip for removing track from wishlist/loved folder"
+ },
+ "collectionRemoved": "\"{trackName}\" removed",
+ "@collectionRemoved": {
+ "description": "Snackbar after removing a track from a collection",
+ "placeholders": {
+ "trackName": {
+ "type": "String"
+ }
+ }
+ },
+ "collectionAddedToLoved": "\"{trackName}\" added to Loved",
+ "@collectionAddedToLoved": {
+ "description": "Snackbar after adding track to loved folder",
+ "placeholders": {
+ "trackName": {
+ "type": "String"
+ }
+ }
+ },
+ "collectionRemovedFromLoved": "\"{trackName}\" removed from Loved",
+ "@collectionRemovedFromLoved": {
+ "description": "Snackbar after removing track from loved folder",
+ "placeholders": {
+ "trackName": {
+ "type": "String"
+ }
+ }
+ },
+ "collectionAddedToWishlist": "\"{trackName}\" added to Wishlist",
+ "@collectionAddedToWishlist": {
+ "description": "Snackbar after adding track to wishlist",
+ "placeholders": {
+ "trackName": {
+ "type": "String"
+ }
+ }
+ },
+ "collectionRemovedFromWishlist": "\"{trackName}\" removed from Wishlist",
+ "@collectionRemovedFromWishlist": {
+ "description": "Snackbar after removing track from wishlist",
+ "placeholders": {
+ "trackName": {
+ "type": "String"
+ }
+ }
+ },
+ "trackOptionAddToLoved": "Add to Loved",
+ "@trackOptionAddToLoved": {
+ "description": "Bottom sheet action label - add track to loved folder"
+ },
+ "trackOptionRemoveFromLoved": "Remove from Loved",
+ "@trackOptionRemoveFromLoved": {
+ "description": "Bottom sheet action label - remove track from loved folder"
+ },
+ "trackOptionAddToWishlist": "Add to Wishlist",
+ "@trackOptionAddToWishlist": {
+ "description": "Bottom sheet action label - add track to wishlist"
+ },
+ "trackOptionRemoveFromWishlist": "Remove from Wishlist",
+ "@trackOptionRemoveFromWishlist": {
+ "description": "Bottom sheet action label - remove track from wishlist"
+ },
+ "collectionPlaylistChangeCover": "Change cover image",
+ "@collectionPlaylistChangeCover": {
+ "description": "Bottom sheet action to pick a custom cover image for a playlist"
+ },
+ "collectionPlaylistRemoveCover": "Remove cover image",
+ "@collectionPlaylistRemoveCover": {
+ "description": "Bottom sheet action to remove custom cover image from a playlist"
+ },
+ "selectionShareCount": "Share {count} {count, plural, =1{track} other{tracks}}",
+ "@selectionShareCount": {
+ "description": "Share button text with count in selection mode",
+ "placeholders": {
+ "count": {
+ "type": "int"
+ }
+ }
+ },
+ "selectionShareNoFiles": "No shareable files found",
+ "@selectionShareNoFiles": {
+ "description": "Snackbar when no selected files exist on disk"
+ },
+ "selectionConvertCount": "Convert {count} {count, plural, =1{track} other{tracks}}",
+ "@selectionConvertCount": {
+ "description": "Convert button text with count in selection mode",
+ "placeholders": {
+ "count": {
+ "type": "int"
+ }
+ }
+ },
+ "selectionConvertNoConvertible": "No convertible tracks selected",
+ "@selectionConvertNoConvertible": {
+ "description": "Snackbar when no selected tracks support conversion"
+ },
+ "selectionBatchConvertConfirmTitle": "Batch Convert",
+ "@selectionBatchConvertConfirmTitle": {
+ "description": "Confirmation dialog title for batch conversion"
+ },
+ "selectionBatchConvertConfirmMessage": "Convert {count} {count, plural, =1{track} other{tracks}} to {format} at {bitrate}?\n\nOriginal files will be deleted after conversion.",
+ "@selectionBatchConvertConfirmMessage": {
+ "description": "Confirmation dialog message for batch conversion",
+ "placeholders": {
+ "count": {
+ "type": "int"
+ },
+ "format": {
+ "type": "String"
+ },
+ "bitrate": {
+ "type": "String"
+ }
+ }
+ },
+ "selectionBatchConvertProgress": "Converting {current} of {total}...",
+ "@selectionBatchConvertProgress": {
+ "description": "Snackbar during batch conversion progress",
+ "placeholders": {
+ "current": {
+ "type": "int"
+ },
+ "total": {
+ "type": "int"
+ }
+ }
+ },
+ "selectionBatchConvertSuccess": "Converted {success} of {total} tracks to {format}",
+ "@selectionBatchConvertSuccess": {
+ "description": "Snackbar after batch conversion completes",
+ "placeholders": {
+ "success": {
+ "type": "int"
+ },
+ "total": {
+ "type": "int"
+ },
+ "format": {
+ "type": "String"
+ }
+ }
+ },
"downloadedAlbumDownloadedCount": "{count} downloaded",
"@downloadedAlbumDownloadedCount": {
"description": "Downloaded tracks count badge",
@@ -2800,4 +3102,4 @@
"@downloadUseAlbumArtistForFoldersTrackSubtitle": {
"description": "Subtitle when Track Artist is used for folder naming"
}
-}
+}
\ No newline at end of file
diff --git a/lib/l10n/arb/app_id.arb b/lib/l10n/arb/app_id.arb
index 0d7caf67..de9d90d0 100644
--- a/lib/l10n/arb/app_id.arb
+++ b/lib/l10n/arb/app_id.arb
@@ -9,7 +9,7 @@
"@navHome": {
"description": "Bottom navigation - Home tab"
},
- "navLibrary": "Library",
+ "navLibrary": "Pustaka",
"@navLibrary": {
"description": "Bottom navigation - Library tab"
},
@@ -49,7 +49,7 @@
"@historyFilterSingles": {
"description": "Filter chip - show singles only"
},
- "historySearchHint": "Search history...",
+ "historySearchHint": "Cari riwayat...",
"@historySearchHint": {
"description": "Search bar placeholder in history"
},
@@ -125,7 +125,7 @@
"@appearanceHistoryViewList": {
"description": "List layout option"
},
- "appearanceHistoryViewGrid": "Grid",
+ "appearanceHistoryViewGrid": "Kisi",
"@appearanceHistoryViewGrid": {
"description": "Grid layout option"
},
@@ -154,7 +154,7 @@
"@optionsSwitchBack": {
"description": "Hint to switch back to built-in providers"
},
- "optionsAutoFallback": "Auto Fallback",
+ "optionsAutoFallback": "Cadangan Otomatis",
"@optionsAutoFallback": {
"description": "Auto-retry with other services"
},
@@ -267,7 +267,7 @@
"@optionsSpotifyCredentials": {
"description": "Spotify API credentials setting"
},
- "optionsSpotifyCredentialsConfigured": "Client ID: {clientId}...",
+ "optionsSpotifyCredentialsConfigured": "ID Klien: {clientId}...",
"@optionsSpotifyCredentialsConfigured": {
"description": "Shows configured client ID preview",
"placeholders": {
@@ -284,7 +284,7 @@
"@optionsSpotifyWarning": {
"description": "Info about Spotify API requirement"
},
- "optionsSpotifyDeprecationWarning": "Spotify search will be deprecated on March 3, 2026 due to Spotify API changes. Please switch to Deezer.",
+ "optionsSpotifyDeprecationWarning": "Pencarian Spotify akan dihentikan pada 3 Maret 2026 karena perubahan API Spotify. Silakan beralih ke Deezer.",
"@optionsSpotifyDeprecationWarning": {
"description": "Warning about Spotify API deprecation"
},
@@ -358,7 +358,7 @@
"@aboutLogoArtist": {
"description": "Role description for logo artist"
},
- "aboutTranslators": "Translators",
+ "aboutTranslators": "Penerjemah",
"@aboutTranslators": {
"description": "Section for translators"
},
@@ -394,23 +394,23 @@
"@aboutFeatureRequestSubtitle": {
"description": "Subtitle for feature request"
},
- "aboutTelegramChannel": "Telegram Channel",
+ "aboutTelegramChannel": "Saluran Telegram",
"@aboutTelegramChannel": {
"description": "Link to Telegram channel"
},
- "aboutTelegramChannelSubtitle": "Announcements and updates",
+ "aboutTelegramChannelSubtitle": "Pengumuman dan pembaruan",
"@aboutTelegramChannelSubtitle": {
"description": "Subtitle for Telegram channel"
},
- "aboutTelegramChat": "Telegram Community",
+ "aboutTelegramChat": "Komunitas Telegram",
"@aboutTelegramChat": {
"description": "Link to Telegram chat group"
},
- "aboutTelegramChatSubtitle": "Chat with other users",
+ "aboutTelegramChatSubtitle": "Berbincang dengan pengguna lain",
"@aboutTelegramChatSubtitle": {
"description": "Subtitle for Telegram chat"
},
- "aboutSocial": "Social",
+ "aboutSocial": "Sosial",
"@aboutSocial": {
"description": "Section for social links"
},
@@ -430,7 +430,7 @@
"@aboutSachinsenalDesc": {
"description": "Credit description for sachinsenal0x64"
},
- "aboutSjdonadoDesc": "Creator of I Don't Have Spotify (IDHS). The fallback link resolver that saves the day!",
+ "aboutSjdonadoDesc": "Pencipta I Don't Have Spotify (IDHS). Penyelesai tautan cadangan yang menyelamatkan keadaan!",
"@aboutSjdonadoDesc": {
"description": "Credit description for sjdonado"
},
@@ -446,7 +446,7 @@
"@aboutSpotiSaver": {
"description": "Name of SpotiSaver API service - DO NOT TRANSLATE"
},
- "aboutSpotiSaverDesc": "Tidal Hi-Res FLAC streaming endpoints. A key piece of the lossless puzzle!",
+ "aboutSpotiSaverDesc": "Tidal perangkat streaming FLAC resolusi tinggi. Bagian penting dari teka-teki tanpa kehilangan kualitas!",
"@aboutSpotiSaverDesc": {
"description": "Credit for SpotiSaver API"
},
@@ -579,7 +579,7 @@
"@setupIosEmptyFolderWarning": {
"description": "iOS folder selection warning"
},
- "setupIcloudNotSupported": "iCloud Drive is not supported. Please use the app Documents folder.",
+ "setupIcloudNotSupported": "iCloud Drive tidak didukung. Silakan gunakan folder Dokumen di aplikasi.",
"@setupIcloudNotSupported": {
"description": "Error when user selects iCloud Drive on iOS"
},
@@ -742,7 +742,7 @@
"description": "Dialog title - import CSV playlist"
},
"dialogImportPlaylistMessage": "Ditemukan {count} lagu di CSV. Tambahkan ke antrian unduhan?",
- "csvImportTracks": "{count} tracks from CSV",
+ "csvImportTracks": "{count} trek dari CSV",
"@csvImportTracks": {
"description": "Label shown in quality picker for CSV import",
"placeholders": {
@@ -786,7 +786,7 @@
}
}
},
- "snackbarAlreadyInLibrary": "\"{trackName}\" already exists in your library",
+ "snackbarAlreadyInLibrary": "\"{trackName}\" sudah ada di perpustakaan Anda",
"@snackbarAlreadyInLibrary": {
"description": "Snackbar - track already exists in local library",
"placeholders": {
@@ -897,6 +897,18 @@
"@errorNoTracksFound": {
"description": "Error - search returned no results"
},
+ "errorUrlNotRecognized": "Link tidak dikenali",
+ "@errorUrlNotRecognized": {
+ "description": "Error title - URL not handled by any extension or service"
+ },
+ "errorUrlNotRecognizedMessage": "Link ini tidak didukung. Pastikan URL benar dan ekstensi yang kompatibel sudah terpasang.",
+ "@errorUrlNotRecognizedMessage": {
+ "description": "Error message - URL not recognized explanation"
+ },
+ "errorUrlFetchFailed": "Gagal memuat konten dari link ini. Silakan coba lagi.",
+ "@errorUrlFetchFailed": {
+ "description": "Error message - generic URL fetch failure"
+ },
"errorMissingExtensionSource": "Tidak dapat memuat {item}: sumber ekstensi tidak ada",
"@errorMissingExtensionSource": {
"description": "Error - extension source not available",
@@ -991,11 +1003,11 @@
"@filenameFormat": {
"description": "Setting title - filename pattern"
},
- "filenameShowAdvancedTags": "Tampilkan tag lanjutan",
+ "filenameShowAdvancedTags": "Show advanced tags",
"@filenameShowAdvancedTags": {
"description": "Toggle label for showing advanced filename tags"
},
- "filenameShowAdvancedTagsDescription": "Aktifkan tag format untuk padding nomor lagu dan pola tanggal",
+ "filenameShowAdvancedTagsDescription": "Enable formatted tags for track padding and date patterns",
"@filenameShowAdvancedTagsDescription": {
"description": "Description for advanced filename tag toggle"
},
@@ -1757,11 +1769,11 @@
"@youtubeQualityNote": {
"description": "Note for YouTube service explaining lossy-only quality"
},
- "youtubeOpusBitrateTitle": "Bitrate Opus YouTube",
+ "youtubeOpusBitrateTitle": "YouTube Opus Bitrate",
"@youtubeOpusBitrateTitle": {
"description": "Title for YouTube Opus bitrate setting"
},
- "youtubeMp3BitrateTitle": "Bitrate MP3 YouTube",
+ "youtubeMp3BitrateTitle": "YouTube MP3 Bitrate",
"@youtubeMp3BitrateTitle": {
"description": "Title for YouTube MP3 bitrate setting"
},
@@ -2214,7 +2226,7 @@
"@libraryAboutDescription": {
"description": "Description of local library feature"
},
- "libraryTracksUnit": "{count, plural, =1{trek} other{trek}}",
+ "libraryTracksUnit": "{count, plural, =1{track} other{tracks}}",
"@libraryTracksUnit": {
"description": "Unit label for tracks count (without the number itself)",
"placeholders": {
@@ -2808,11 +2820,11 @@
"@trackConvertFailed": {
"description": "Snackbar when conversion fails"
},
- "actionCreate": "Buat",
+ "actionCreate": "Create",
"@actionCreate": {
"description": "Generic action button - create"
},
- "collectionFoldersTitle": "Folder saya",
+ "collectionFoldersTitle": "My folders",
"@collectionFoldersTitle": {
"description": "Library section title for custom folders"
},
@@ -2824,7 +2836,7 @@
"@collectionLoved": {
"description": "Custom folder for favorite tracks"
},
- "collectionPlaylists": "Playlist",
+ "collectionPlaylists": "Playlists",
"@collectionPlaylists": {
"description": "Custom user playlists folder"
},
@@ -2832,23 +2844,23 @@
"@collectionPlaylist": {
"description": "Single playlist label"
},
- "collectionAddToPlaylist": "Tambahkan ke playlist",
+ "collectionAddToPlaylist": "Add to playlist",
"@collectionAddToPlaylist": {
"description": "Action to add a track to user playlist"
},
- "collectionCreatePlaylist": "Buat playlist",
+ "collectionCreatePlaylist": "Create playlist",
"@collectionCreatePlaylist": {
"description": "Action to create a new playlist"
},
- "collectionNoPlaylistsYet": "Belum ada playlist",
+ "collectionNoPlaylistsYet": "No playlists yet",
"@collectionNoPlaylistsYet": {
"description": "Empty state title when user has no playlists"
},
- "collectionNoPlaylistsSubtitle": "Buat playlist untuk mulai mengategorikan lagu",
+ "collectionNoPlaylistsSubtitle": "Create a playlist to start categorizing tracks",
"@collectionNoPlaylistsSubtitle": {
"description": "Empty state subtitle when user has no playlists"
},
- "collectionPlaylistTracks": "{count, plural, =1{1 lagu} other{{count} lagu}}",
+ "collectionPlaylistTracks": "{count, plural, =1{1 track} other{{count} tracks}}",
"@collectionPlaylistTracks": {
"description": "Track count label for custom playlists",
"placeholders": {
@@ -2857,7 +2869,7 @@
}
}
},
- "collectionAddedToPlaylist": "Ditambahkan ke \"{playlistName}\"",
+ "collectionAddedToPlaylist": "Added to \"{playlistName}\"",
"@collectionAddedToPlaylist": {
"description": "Snackbar after adding track to playlist",
"placeholders": {
@@ -2866,7 +2878,7 @@
}
}
},
- "collectionAlreadyInPlaylist": "Sudah ada di \"{playlistName}\"",
+ "collectionAlreadyInPlaylist": "Already in \"{playlistName}\"",
"@collectionAlreadyInPlaylist": {
"description": "Snackbar when track already exists in playlist",
"placeholders": {
@@ -2875,27 +2887,27 @@
}
}
},
- "collectionPlaylistCreated": "Playlist berhasil dibuat",
+ "collectionPlaylistCreated": "Playlist created",
"@collectionPlaylistCreated": {
"description": "Snackbar after creating playlist"
},
- "collectionPlaylistNameHint": "Nama playlist",
+ "collectionPlaylistNameHint": "Playlist name",
"@collectionPlaylistNameHint": {
"description": "Hint text for playlist name input"
},
- "collectionPlaylistNameRequired": "Nama playlist wajib diisi",
+ "collectionPlaylistNameRequired": "Playlist name is required",
"@collectionPlaylistNameRequired": {
"description": "Validation error for empty playlist name"
},
- "collectionRenamePlaylist": "Ubah nama playlist",
+ "collectionRenamePlaylist": "Rename playlist",
"@collectionRenamePlaylist": {
"description": "Action to rename playlist"
},
- "collectionDeletePlaylist": "Hapus playlist",
+ "collectionDeletePlaylist": "Delete playlist",
"@collectionDeletePlaylist": {
"description": "Action to delete playlist"
},
- "collectionDeletePlaylistMessage": "Hapus \"{playlistName}\" beserta semua lagunya?",
+ "collectionDeletePlaylistMessage": "Delete \"{playlistName}\" and all tracks inside it?",
"@collectionDeletePlaylistMessage": {
"description": "Confirmation message for deleting playlist",
"placeholders": {
@@ -2904,47 +2916,47 @@
}
}
},
- "collectionPlaylistDeleted": "Playlist dihapus",
+ "collectionPlaylistDeleted": "Playlist deleted",
"@collectionPlaylistDeleted": {
"description": "Snackbar after deleting playlist"
},
- "collectionPlaylistRenamed": "Nama playlist diperbarui",
+ "collectionPlaylistRenamed": "Playlist renamed",
"@collectionPlaylistRenamed": {
"description": "Snackbar after renaming playlist"
},
- "collectionWishlistEmptyTitle": "Wishlist masih kosong",
+ "collectionWishlistEmptyTitle": "Wishlist is empty",
"@collectionWishlistEmptyTitle": {
"description": "Wishlist empty state title"
},
- "collectionWishlistEmptySubtitle": "Tap + di lagu untuk menyimpan yang ingin diunduh nanti",
+ "collectionWishlistEmptySubtitle": "Tap + on tracks to save what you want to download later",
"@collectionWishlistEmptySubtitle": {
"description": "Wishlist empty state subtitle"
},
- "collectionLovedEmptyTitle": "Folder Loved masih kosong",
+ "collectionLovedEmptyTitle": "Loved folder is empty",
"@collectionLovedEmptyTitle": {
"description": "Loved empty state title"
},
- "collectionLovedEmptySubtitle": "Tap love di lagu untuk menyimpan favoritmu",
+ "collectionLovedEmptySubtitle": "Tap love on tracks to keep your favorites",
"@collectionLovedEmptySubtitle": {
"description": "Loved empty state subtitle"
},
- "collectionPlaylistEmptyTitle": "Playlist masih kosong",
+ "collectionPlaylistEmptyTitle": "Playlist is empty",
"@collectionPlaylistEmptyTitle": {
"description": "Playlist empty state title"
},
- "collectionPlaylistEmptySubtitle": "Tekan lama tombol + pada lagu untuk menambahkannya ke sini",
+ "collectionPlaylistEmptySubtitle": "Long-press + on any track to add it here",
"@collectionPlaylistEmptySubtitle": {
"description": "Playlist empty state subtitle"
},
- "collectionRemoveFromPlaylist": "Hapus dari playlist",
+ "collectionRemoveFromPlaylist": "Remove from playlist",
"@collectionRemoveFromPlaylist": {
"description": "Tooltip for removing track from playlist"
},
- "collectionRemoveFromFolder": "Hapus dari folder",
+ "collectionRemoveFromFolder": "Remove from folder",
"@collectionRemoveFromFolder": {
"description": "Tooltip for removing track from wishlist/loved folder"
},
- "collectionRemoved": "\"{trackName}\" dihapus",
+ "collectionRemoved": "\"{trackName}\" removed",
"@collectionRemoved": {
"description": "Snackbar after removing a track from a collection",
"placeholders": {
@@ -2953,7 +2965,7 @@
}
}
},
- "collectionAddedToLoved": "\"{trackName}\" ditambahkan ke Loved",
+ "collectionAddedToLoved": "\"{trackName}\" added to Loved",
"@collectionAddedToLoved": {
"description": "Snackbar after adding track to loved folder",
"placeholders": {
@@ -2962,7 +2974,7 @@
}
}
},
- "collectionRemovedFromLoved": "\"{trackName}\" dihapus dari Loved",
+ "collectionRemovedFromLoved": "\"{trackName}\" removed from Loved",
"@collectionRemovedFromLoved": {
"description": "Snackbar after removing track from loved folder",
"placeholders": {
@@ -2971,7 +2983,7 @@
}
}
},
- "collectionAddedToWishlist": "\"{trackName}\" ditambahkan ke Wishlist",
+ "collectionAddedToWishlist": "\"{trackName}\" added to Wishlist",
"@collectionAddedToWishlist": {
"description": "Snackbar after adding track to wishlist",
"placeholders": {
@@ -2980,7 +2992,7 @@
}
}
},
- "collectionRemovedFromWishlist": "\"{trackName}\" dihapus dari Wishlist",
+ "collectionRemovedFromWishlist": "\"{trackName}\" removed from Wishlist",
"@collectionRemovedFromWishlist": {
"description": "Snackbar after removing track from wishlist",
"placeholders": {
@@ -2989,31 +3001,31 @@
}
}
},
- "trackOptionAddToLoved": "Tambahkan ke Loved",
+ "trackOptionAddToLoved": "Add to Loved",
"@trackOptionAddToLoved": {
"description": "Bottom sheet action label - add track to loved folder"
},
- "trackOptionRemoveFromLoved": "Hapus dari Loved",
+ "trackOptionRemoveFromLoved": "Remove from Loved",
"@trackOptionRemoveFromLoved": {
"description": "Bottom sheet action label - remove track from loved folder"
},
- "trackOptionAddToWishlist": "Tambahkan ke Wishlist",
+ "trackOptionAddToWishlist": "Add to Wishlist",
"@trackOptionAddToWishlist": {
"description": "Bottom sheet action label - add track to wishlist"
},
- "trackOptionRemoveFromWishlist": "Hapus dari Wishlist",
+ "trackOptionRemoveFromWishlist": "Remove from Wishlist",
"@trackOptionRemoveFromWishlist": {
"description": "Bottom sheet action label - remove track from wishlist"
},
- "collectionPlaylistChangeCover": "Ubah gambar sampul",
+ "collectionPlaylistChangeCover": "Change cover image",
"@collectionPlaylistChangeCover": {
"description": "Bottom sheet action to pick a custom cover image for a playlist"
},
- "collectionPlaylistRemoveCover": "Hapus gambar sampul",
+ "collectionPlaylistRemoveCover": "Remove cover image",
"@collectionPlaylistRemoveCover": {
"description": "Bottom sheet action to remove custom cover image from a playlist"
},
- "selectionShareCount": "Bagikan {count} {count, plural, =1{trek} other{trek}}",
+ "selectionShareCount": "Share {count} {count, plural, =1{track} other{tracks}}",
"@selectionShareCount": {
"description": "Share button text with count in selection mode",
"placeholders": {
@@ -3022,11 +3034,11 @@
}
}
},
- "selectionShareNoFiles": "Tidak ada file yang dapat dibagikan",
+ "selectionShareNoFiles": "No shareable files found",
"@selectionShareNoFiles": {
"description": "Snackbar when no selected files exist on disk"
},
- "selectionConvertCount": "Konversi {count} {count, plural, =1{trek} other{trek}}",
+ "selectionConvertCount": "Convert {count} {count, plural, =1{track} other{tracks}}",
"@selectionConvertCount": {
"description": "Convert button text with count in selection mode",
"placeholders": {
@@ -3035,15 +3047,15 @@
}
}
},
- "selectionConvertNoConvertible": "Tidak ada trek yang dapat dikonversi dipilih",
+ "selectionConvertNoConvertible": "No convertible tracks selected",
"@selectionConvertNoConvertible": {
"description": "Snackbar when no selected tracks support conversion"
},
- "selectionBatchConvertConfirmTitle": "Konversi Massal",
+ "selectionBatchConvertConfirmTitle": "Batch Convert",
"@selectionBatchConvertConfirmTitle": {
"description": "Confirmation dialog title for batch conversion"
},
- "selectionBatchConvertConfirmMessage": "Konversi {count} {count, plural, =1{trek} other{trek}} ke {format} pada {bitrate}?\n\nFile asli akan dihapus setelah konversi.",
+ "selectionBatchConvertConfirmMessage": "Convert {count} {count, plural, =1{track} other{tracks}} to {format} at {bitrate}?\n\nOriginal files will be deleted after conversion.",
"@selectionBatchConvertConfirmMessage": {
"description": "Confirmation dialog message for batch conversion",
"placeholders": {
@@ -3058,7 +3070,7 @@
}
}
},
- "selectionBatchConvertProgress": "Mengonversi {current} dari {total}...",
+ "selectionBatchConvertProgress": "Converting {current} of {total}...",
"@selectionBatchConvertProgress": {
"description": "Snackbar during batch conversion progress",
"placeholders": {
@@ -3070,7 +3082,7 @@
}
}
},
- "selectionBatchConvertSuccess": "Berhasil mengonversi {success} dari {total} trek ke {format}",
+ "selectionBatchConvertSuccess": "Converted {success} of {total} tracks to {format}",
"@selectionBatchConvertSuccess": {
"description": "Snackbar after batch conversion completes",
"placeholders": {
@@ -3102,4 +3114,4 @@
"@downloadUseAlbumArtistForFoldersTrackSubtitle": {
"description": "Subtitle when Track Artist is used for folder naming"
}
-}
+}
\ No newline at end of file
diff --git a/lib/l10n/arb/app_ja.arb b/lib/l10n/arb/app_ja.arb
index b23b0de3..268d8950 100644
--- a/lib/l10n/arb/app_ja.arb
+++ b/lib/l10n/arb/app_ja.arb
@@ -9,7 +9,7 @@
"@navHome": {
"description": "Bottom navigation - Home tab"
},
- "navLibrary": "Library",
+ "navLibrary": "ライブラリ",
"@navLibrary": {
"description": "Bottom navigation - Library tab"
},
@@ -198,7 +198,7 @@
"@optionsConcurrentSequential": {
"description": "Download one at a time"
},
- "optionsConcurrentParallel": "{count} parallel downloads",
+ "optionsConcurrentParallel": "{count} 件の分割ダウンロード",
"@optionsConcurrentParallel": {
"description": "Multiple parallel downloads",
"placeholders": {
@@ -991,6 +991,14 @@
"@filenameFormat": {
"description": "Setting title - filename pattern"
},
+ "filenameShowAdvancedTags": "Show advanced tags",
+ "@filenameShowAdvancedTags": {
+ "description": "Toggle label for showing advanced filename tags"
+ },
+ "filenameShowAdvancedTagsDescription": "Enable formatted tags for track padding and date patterns",
+ "@filenameShowAdvancedTagsDescription": {
+ "description": "Description for advanced filename tag toggle"
+ },
"folderOrganizationNone": "構成がありません",
"@folderOrganizationNone": {
"description": "Folder option - flat structure"
@@ -1455,7 +1463,7 @@
"@trackLyricsLoadFailed": {
"description": "Message when lyrics loading fails"
},
- "trackEmbedLyrics": "Embed Lyrics",
+ "trackEmbedLyrics": "歌詞を埋め込む",
"@trackEmbedLyrics": {
"description": "Action - embed lyrics into audio file"
},
@@ -1749,6 +1757,14 @@
"@youtubeQualityNote": {
"description": "Note for YouTube service explaining lossy-only quality"
},
+ "youtubeOpusBitrateTitle": "YouTube Opus のビットレート",
+ "@youtubeOpusBitrateTitle": {
+ "description": "Title for YouTube Opus bitrate setting"
+ },
+ "youtubeMp3BitrateTitle": "YouTube MP3 のビットレート",
+ "@youtubeMp3BitrateTitle": {
+ "description": "Title for YouTube MP3 bitrate setting"
+ },
"downloadAskBeforeDownload": "ダウンロード前に確認する",
"@downloadAskBeforeDownload": {
"description": "Setting - show quality picker"
@@ -1805,7 +1821,7 @@
"@queueClearAllMessage": {
"description": "Clear queue confirmation"
},
- "settingsAutoExportFailed": "Auto-export failed downloads",
+ "settingsAutoExportFailed": "ダウンロードの自動エクスポートに失敗しました",
"@settingsAutoExportFailed": {
"description": "Setting toggle for auto-export"
},
@@ -1813,15 +1829,15 @@
"@settingsAutoExportFailedSubtitle": {
"description": "Subtitle for auto-export setting"
},
- "settingsDownloadNetwork": "Download Network",
+ "settingsDownloadNetwork": "ダウンロードネットワーク",
"@settingsDownloadNetwork": {
"description": "Setting for network type preference"
},
- "settingsDownloadNetworkAny": "WiFi + Mobile Data",
+ "settingsDownloadNetworkAny": "Wi-Fi + モバイルデータ",
"@settingsDownloadNetworkAny": {
"description": "Network option - use any connection"
},
- "settingsDownloadNetworkWifiOnly": "WiFi Only",
+ "settingsDownloadNetworkWifiOnly": "Wi-Fi のみ",
"@settingsDownloadNetworkWifiOnly": {
"description": "Network option - only use WiFi"
},
@@ -1861,7 +1877,7 @@
"@albumFolderYearAlbumSubtitle": {
"description": "Folder structure example"
},
- "albumFolderArtistAlbumSingles": "Artist / Album + Singles",
+ "albumFolderArtistAlbumSingles": "アーティスト / アルバム + シングル",
"@albumFolderArtistAlbumSingles": {
"description": "Album folder option with singles inside artist"
},
@@ -1942,7 +1958,7 @@
"@recentEmpty": {
"description": "Empty state text for recent access list"
},
- "recentShowAllDownloads": "Show All Downloads",
+ "recentShowAllDownloads": "すべてのダウンロードを表示",
"@recentShowAllDownloads": {
"description": "Button label to unhide hidden downloads in recent access"
},
@@ -2074,11 +2090,11 @@
"@discographyFailedToFetch": {
"description": "Error - some albums failed to load"
},
- "sectionStorageAccess": "Storage Access",
+ "sectionStorageAccess": "ストレージアクセス",
"@sectionStorageAccess": {
"description": "Section header for storage access settings"
},
- "allFilesAccess": "All Files Access",
+ "allFilesAccess": "すべてのファイルへのアクセス",
"@allFilesAccess": {
"description": "Toggle for MANAGE_EXTERNAL_STORAGE permission"
},
@@ -2102,7 +2118,7 @@
"@allFilesAccessDisabledMessage": {
"description": "Snackbar message when user disables all files access"
},
- "settingsLocalLibrary": "Local Library",
+ "settingsLocalLibrary": "ローカルライブラリ",
"@settingsLocalLibrary": {
"description": "Settings menu item - local library"
},
@@ -2110,7 +2126,7 @@
"@settingsLocalLibrarySubtitle": {
"description": "Subtitle for local library settings"
},
- "settingsCache": "Storage & Cache",
+ "settingsCache": "ストレージとキャッシュ",
"@settingsCache": {
"description": "Settings menu item - cache management"
},
@@ -2118,15 +2134,15 @@
"@settingsCacheSubtitle": {
"description": "Subtitle for cache management menu"
},
- "libraryTitle": "Local Library",
+ "libraryTitle": "ローカルライブラリ",
"@libraryTitle": {
"description": "Library settings page title"
},
- "libraryScanSettings": "Scan Settings",
+ "libraryScanSettings": "スキャン設定",
"@libraryScanSettings": {
"description": "Section header for scan settings"
},
- "libraryEnableLocalLibrary": "Enable Local Library",
+ "libraryEnableLocalLibrary": "ローカルライブラリを有効",
"@libraryEnableLocalLibrary": {
"description": "Toggle to enable library scanning"
},
@@ -2134,11 +2150,11 @@
"@libraryEnableLocalLibrarySubtitle": {
"description": "Subtitle for enable toggle"
},
- "libraryFolder": "Library Folder",
+ "libraryFolder": "ライブラリのフォルダ",
"@libraryFolder": {
"description": "Folder selection setting"
},
- "libraryFolderHint": "Tap to select folder",
+ "libraryFolderHint": "タップでフォルダを選択",
"@libraryFolderHint": {
"description": "Placeholder when no folder selected"
},
@@ -2150,15 +2166,15 @@
"@libraryShowDuplicateIndicatorSubtitle": {
"description": "Subtitle for duplicate indicator toggle"
},
- "libraryActions": "Actions",
+ "libraryActions": "アクション",
"@libraryActions": {
"description": "Section header for library actions"
},
- "libraryScan": "Scan Library",
+ "libraryScan": "ライブラリをスキャン",
"@libraryScan": {
"description": "Button to start library scan"
},
- "libraryScanSubtitle": "Scan for audio files",
+ "libraryScanSubtitle": "オーディオファイルをスキャン",
"@libraryScanSubtitle": {
"description": "Subtitle for scan button"
},
@@ -2174,7 +2190,7 @@
"@libraryCleanupMissingFilesSubtitle": {
"description": "Subtitle for cleanup button"
},
- "libraryClear": "Clear Library",
+ "libraryClear": "ライブラリを消去",
"@libraryClear": {
"description": "Button to clear all library entries"
},
@@ -2182,7 +2198,7 @@
"@libraryClearSubtitle": {
"description": "Subtitle for clear button"
},
- "libraryClearConfirmTitle": "Clear Library",
+ "libraryClearConfirmTitle": "ライブラリを消去",
"@libraryClearConfirmTitle": {
"description": "Dialog title for clear confirmation"
},
@@ -2190,7 +2206,7 @@
"@libraryClearConfirmMessage": {
"description": "Dialog message for clear confirmation"
},
- "libraryAbout": "About Local Library",
+ "libraryAbout": "ローカルライブラリについて",
"@libraryAbout": {
"description": "Section header for about info"
},
@@ -2198,7 +2214,16 @@
"@libraryAboutDescription": {
"description": "Description of local library feature"
},
- "libraryLastScanned": "Last scanned: {time}",
+ "libraryTracksUnit": "{count, plural, =1{track} other{tracks}}",
+ "@libraryTracksUnit": {
+ "description": "Unit label for tracks count (without the number itself)",
+ "placeholders": {
+ "count": {
+ "type": "int"
+ }
+ }
+ },
+ "libraryLastScanned": "最終スキャン: {time}",
"@libraryLastScanned": {
"description": "Last scan time display",
"placeholders": {
@@ -2211,7 +2236,7 @@
"@libraryLastScannedNever": {
"description": "Shown when library has never been scanned"
},
- "libraryScanning": "Scanning...",
+ "libraryScanning": "スキャン中...",
"@libraryScanning": {
"description": "Status during scan"
},
@@ -2227,7 +2252,7 @@
}
}
},
- "libraryInLibrary": "In Library",
+ "libraryInLibrary": "ライブラリ内",
"@libraryInLibrary": {
"description": "Badge shown on tracks that exist in local library"
},
@@ -2244,7 +2269,7 @@
"@libraryCleared": {
"description": "Snackbar after clearing library"
},
- "libraryStorageAccessRequired": "Storage Access Required",
+ "libraryStorageAccessRequired": "ストレージアクセスが必要です",
"@libraryStorageAccessRequired": {
"description": "Dialog title for storage permission"
},
@@ -2256,47 +2281,47 @@
"@libraryFolderNotExist": {
"description": "Error when folder doesn't exist"
},
- "librarySourceDownloaded": "Downloaded",
+ "librarySourceDownloaded": "ダウンロード済み",
"@librarySourceDownloaded": {
"description": "Badge for tracks downloaded via SpotiFLAC"
},
- "librarySourceLocal": "Local",
+ "librarySourceLocal": "ローカル",
"@librarySourceLocal": {
"description": "Badge for tracks from local library scan"
},
- "libraryFilterAll": "All",
+ "libraryFilterAll": "すべて",
"@libraryFilterAll": {
"description": "Filter chip - show all library items"
},
- "libraryFilterDownloaded": "Downloaded",
+ "libraryFilterDownloaded": "ダウンロード済み",
"@libraryFilterDownloaded": {
"description": "Filter chip - show only downloaded items"
},
- "libraryFilterLocal": "Local",
+ "libraryFilterLocal": "ローカル",
"@libraryFilterLocal": {
"description": "Filter chip - show only local library items"
},
- "libraryFilterTitle": "Filters",
+ "libraryFilterTitle": "フィルター",
"@libraryFilterTitle": {
"description": "Filter bottom sheet title"
},
- "libraryFilterReset": "Reset",
+ "libraryFilterReset": "リセット",
"@libraryFilterReset": {
"description": "Reset all filters button"
},
- "libraryFilterApply": "Apply",
+ "libraryFilterApply": "適用",
"@libraryFilterApply": {
"description": "Apply filters button"
},
- "libraryFilterSource": "Source",
+ "libraryFilterSource": "ソース",
"@libraryFilterSource": {
"description": "Filter section - source type"
},
- "libraryFilterQuality": "Quality",
+ "libraryFilterQuality": "品質",
"@libraryFilterQuality": {
"description": "Filter section - audio quality"
},
- "libraryFilterQualityHiRes": "Hi-Res (24bit)",
+ "libraryFilterQualityHiRes": "ハイレゾ (24bit)",
"@libraryFilterQualityHiRes": {
"description": "Filter option - high resolution audio"
},
@@ -2308,7 +2333,7 @@
"@libraryFilterQualityLossy": {
"description": "Filter option - lossy compressed audio"
},
- "libraryFilterFormat": "Format",
+ "libraryFilterFormat": "形式",
"@libraryFilterFormat": {
"description": "Filter section - file format"
},
@@ -2328,7 +2353,7 @@
"@timeJustNow": {
"description": "Relative time - less than a minute ago"
},
- "timeMinutesAgo": "{count, plural, =1{1 minute ago} other{{count} minutes ago}}",
+ "timeMinutesAgo": "{count, plural, =1{1 分前} other{{count} 分前}}",
"@timeMinutesAgo": {
"description": "Relative time - minutes ago",
"placeholders": {
@@ -2337,7 +2362,7 @@
}
}
},
- "timeHoursAgo": "{count, plural, =1{1 hour ago} other{{count} hours ago}}",
+ "timeHoursAgo": "{count, plural, =1{1 時間前} other{{count} 時間前}}",
"@timeHoursAgo": {
"description": "Relative time - hours ago",
"placeholders": {
@@ -2346,7 +2371,7 @@
}
}
},
- "tutorialWelcomeTitle": "Welcome to SpotiFLAC!",
+ "tutorialWelcomeTitle": "SpotiFLAC へようこそ!",
"@tutorialWelcomeTitle": {
"description": "Tutorial welcome page title"
},
@@ -2374,7 +2399,7 @@
"@tutorialSearchDesc": {
"description": "Tutorial search page description"
},
- "tutorialDownloadTitle": "Downloading Music",
+ "tutorialDownloadTitle": "音楽をダウンロード中",
"@tutorialDownloadTitle": {
"description": "Tutorial download page title"
},
@@ -2382,7 +2407,7 @@
"@tutorialDownloadDesc": {
"description": "Tutorial download page description"
},
- "tutorialLibraryTitle": "Your Library",
+ "tutorialLibraryTitle": "あなたのライブラリ",
"@tutorialLibraryTitle": {
"description": "Tutorial library page title"
},
@@ -2402,7 +2427,7 @@
"@tutorialLibraryTip3": {
"description": "Tutorial library tip 3"
},
- "tutorialExtensionsTitle": "Extensions",
+ "tutorialExtensionsTitle": "拡張",
"@tutorialExtensionsTitle": {
"description": "Tutorial extensions page title"
},
@@ -2446,7 +2471,7 @@
"@tutorialReadyMessage": {
"description": "Tutorial completion message"
},
- "libraryForceFullScan": "Force Full Scan",
+ "libraryForceFullScan": "強制フルスキャン",
"@libraryForceFullScan": {
"description": "Button to force a complete rescan of library"
},
@@ -2475,11 +2500,11 @@
"@cleanupOrphanedDownloadsNone": {
"description": "Snackbar when no orphans found"
},
- "cacheTitle": "Storage & Cache",
+ "cacheTitle": "ストレージとキャッシュ",
"@cacheTitle": {
"description": "Cache management page title"
},
- "cacheSummaryTitle": "Cache overview",
+ "cacheSummaryTitle": "キャッシュの概要",
"@cacheSummaryTitle": {
"description": "Heading for cache summary card"
},
@@ -2496,15 +2521,15 @@
}
}
},
- "cacheSectionStorage": "Cached Data",
+ "cacheSectionStorage": "キャッシュ済みデータ",
"@cacheSectionStorage": {
"description": "Section header for cache entries"
},
- "cacheSectionMaintenance": "Maintenance",
+ "cacheSectionMaintenance": "メンテナンス",
"@cacheSectionMaintenance": {
"description": "Section header for cleanup actions"
},
- "cacheAppDirectory": "App cache directory",
+ "cacheAppDirectory": "アプリキャッシュのディレクトリ",
"@cacheAppDirectory": {
"description": "Cache item title for app cache directory"
},
@@ -2512,7 +2537,7 @@
"@cacheAppDirectoryDesc": {
"description": "Description of what app cache directory contains"
},
- "cacheTempDirectory": "Temporary directory",
+ "cacheTempDirectory": "一時ディレクトリ",
"@cacheTempDirectory": {
"description": "Cache item title for temporary files directory"
},
@@ -2520,7 +2545,7 @@
"@cacheTempDirectoryDesc": {
"description": "Description of what temporary directory contains"
},
- "cacheCoverImage": "Cover image cache",
+ "cacheCoverImage": "カバー画像のキャッシュ",
"@cacheCoverImage": {
"description": "Cache item title for persistent cover images"
},
@@ -2528,7 +2553,7 @@
"@cacheCoverImageDesc": {
"description": "Description of what cover image cache contains"
},
- "cacheLibraryCover": "Library cover cache",
+ "cacheLibraryCover": "ライブラリのカバーキャッシュ",
"@cacheLibraryCover": {
"description": "Cache item title for local library cover art images"
},
@@ -2556,7 +2581,7 @@
"@cacheCleanupUnusedDesc": {
"description": "Description of what cleanup unused data does"
},
- "cacheNoData": "No cached data",
+ "cacheNoData": "キャッシュデータはありません",
"@cacheNoData": {
"description": "Label when cache category has no data"
},
@@ -2581,7 +2606,7 @@
}
}
},
- "cacheEntries": "{count} entries",
+ "cacheEntries": "{count} 個のエントリ",
"@cacheEntries": {
"description": "Track cache entry count",
"placeholders": {
@@ -2590,7 +2615,7 @@
}
}
},
- "cacheClearSuccess": "Cleared: {target}",
+ "cacheClearSuccess": "消去済み: {target}",
"@cacheClearSuccess": {
"description": "Snackbar after clearing selected cache",
"placeholders": {
@@ -2599,7 +2624,7 @@
}
}
},
- "cacheClearConfirmTitle": "Clear cache?",
+ "cacheClearConfirmTitle": "キャッシュを消去しますか?",
"@cacheClearConfirmTitle": {
"description": "Dialog title before clearing one cache category"
},
@@ -2612,7 +2637,7 @@
}
}
},
- "cacheClearAllConfirmTitle": "Clear all cache?",
+ "cacheClearAllConfirmTitle": "すべてのキャッシュを消去しますか?",
"@cacheClearAllConfirmTitle": {
"description": "Dialog title before clearing all caches"
},
@@ -2620,11 +2645,11 @@
"@cacheClearAllConfirmMessage": {
"description": "Dialog message before clearing all caches"
},
- "cacheClearAll": "Clear all cache",
+ "cacheClearAll": "すべてのキャッシュを消去",
"@cacheClearAll": {
"description": "Button label to clear all caches"
},
- "cacheCleanupUnused": "Cleanup unused data",
+ "cacheCleanupUnused": "未使用のデータを削除",
"@cacheCleanupUnused": {
"description": "Action title for cleaning unused entries"
},
@@ -2644,11 +2669,11 @@
}
}
},
- "cacheRefreshStats": "Refresh stats",
+ "cacheRefreshStats": "状態を更新",
"@cacheRefreshStats": {
"description": "Button label to refresh cache statistics"
},
- "trackSaveCoverArt": "Save Cover Art",
+ "trackSaveCoverArt": "カバー画像を保存",
"@trackSaveCoverArt": {
"description": "Menu action - save album cover art as file"
},
@@ -2656,7 +2681,7 @@
"@trackSaveCoverArtSubtitle": {
"description": "Subtitle for save cover art action"
},
- "trackSaveLyrics": "Save Lyrics (.lrc)",
+ "trackSaveLyrics": "歌詞を保存 (.lrc)",
"@trackSaveLyrics": {
"description": "Menu action - save lyrics as .lrc file"
},
@@ -2676,7 +2701,7 @@
"@trackReEnrichOnlineSubtitle": {
"description": "Subtitle for re-enrich metadata action for local items"
},
- "trackEditMetadata": "Edit Metadata",
+ "trackEditMetadata": "メタデータを編集",
"@trackEditMetadata": {
"description": "Menu action - edit embedded metadata"
},
@@ -2718,7 +2743,7 @@
"@trackReEnrichFfmpegFailed": {
"description": "Snackbar when FFmpeg embed fails for MP3/Opus"
},
- "trackSaveFailed": "Failed: {error}",
+ "trackSaveFailed": "失敗: {error}",
"@trackSaveFailed": {
"description": "Snackbar when save operation fails",
"placeholders": {
@@ -2727,27 +2752,27 @@
}
}
},
- "trackConvertFormat": "Convert Format",
+ "trackConvertFormat": "変換の形式",
"@trackConvertFormat": {
"description": "Menu item - convert audio format"
},
- "trackConvertFormatSubtitle": "Convert to MP3 or Opus",
+ "trackConvertFormatSubtitle": "MP3 または Opus に変換",
"@trackConvertFormatSubtitle": {
"description": "Subtitle for convert format menu item"
},
- "trackConvertTitle": "Convert Audio",
+ "trackConvertTitle": "オーディオを変換",
"@trackConvertTitle": {
"description": "Title of convert bottom sheet"
},
- "trackConvertTargetFormat": "Target Format",
+ "trackConvertTargetFormat": "ターゲットの形式",
"@trackConvertTargetFormat": {
"description": "Label for format selection"
},
- "trackConvertBitrate": "Bitrate",
+ "trackConvertBitrate": "ビットレート",
"@trackConvertBitrate": {
"description": "Label for bitrate selection"
},
- "trackConvertConfirmTitle": "Confirm Conversion",
+ "trackConvertConfirmTitle": "変換を確認",
"@trackConvertConfirmTitle": {
"description": "Confirmation dialog title"
},
@@ -2766,7 +2791,7 @@
}
}
},
- "trackConvertConverting": "Converting audio...",
+ "trackConvertConverting": "オーディオを変換中...",
"@trackConvertConverting": {
"description": "Snackbar while converting"
},
@@ -2779,10 +2804,287 @@
}
}
},
- "trackConvertFailed": "Conversion failed",
+ "trackConvertFailed": "変換に失敗しました",
"@trackConvertFailed": {
"description": "Snackbar when conversion fails"
},
+ "actionCreate": "Create",
+ "@actionCreate": {
+ "description": "Generic action button - create"
+ },
+ "collectionFoldersTitle": "My folders",
+ "@collectionFoldersTitle": {
+ "description": "Library section title for custom folders"
+ },
+ "collectionWishlist": "Wishlist",
+ "@collectionWishlist": {
+ "description": "Custom folder for saved tracks to download later"
+ },
+ "collectionLoved": "Loved",
+ "@collectionLoved": {
+ "description": "Custom folder for favorite tracks"
+ },
+ "collectionPlaylists": "Playlists",
+ "@collectionPlaylists": {
+ "description": "Custom user playlists folder"
+ },
+ "collectionPlaylist": "Playlist",
+ "@collectionPlaylist": {
+ "description": "Single playlist label"
+ },
+ "collectionAddToPlaylist": "Add to playlist",
+ "@collectionAddToPlaylist": {
+ "description": "Action to add a track to user playlist"
+ },
+ "collectionCreatePlaylist": "Create playlist",
+ "@collectionCreatePlaylist": {
+ "description": "Action to create a new playlist"
+ },
+ "collectionNoPlaylistsYet": "No playlists yet",
+ "@collectionNoPlaylistsYet": {
+ "description": "Empty state title when user has no playlists"
+ },
+ "collectionNoPlaylistsSubtitle": "Create a playlist to start categorizing tracks",
+ "@collectionNoPlaylistsSubtitle": {
+ "description": "Empty state subtitle when user has no playlists"
+ },
+ "collectionPlaylistTracks": "{count, plural, =1{1 track} other{{count} tracks}}",
+ "@collectionPlaylistTracks": {
+ "description": "Track count label for custom playlists",
+ "placeholders": {
+ "count": {
+ "type": "int"
+ }
+ }
+ },
+ "collectionAddedToPlaylist": "Added to \"{playlistName}\"",
+ "@collectionAddedToPlaylist": {
+ "description": "Snackbar after adding track to playlist",
+ "placeholders": {
+ "playlistName": {
+ "type": "String"
+ }
+ }
+ },
+ "collectionAlreadyInPlaylist": "Already in \"{playlistName}\"",
+ "@collectionAlreadyInPlaylist": {
+ "description": "Snackbar when track already exists in playlist",
+ "placeholders": {
+ "playlistName": {
+ "type": "String"
+ }
+ }
+ },
+ "collectionPlaylistCreated": "Playlist created",
+ "@collectionPlaylistCreated": {
+ "description": "Snackbar after creating playlist"
+ },
+ "collectionPlaylistNameHint": "Playlist name",
+ "@collectionPlaylistNameHint": {
+ "description": "Hint text for playlist name input"
+ },
+ "collectionPlaylistNameRequired": "Playlist name is required",
+ "@collectionPlaylistNameRequired": {
+ "description": "Validation error for empty playlist name"
+ },
+ "collectionRenamePlaylist": "Rename playlist",
+ "@collectionRenamePlaylist": {
+ "description": "Action to rename playlist"
+ },
+ "collectionDeletePlaylist": "Delete playlist",
+ "@collectionDeletePlaylist": {
+ "description": "Action to delete playlist"
+ },
+ "collectionDeletePlaylistMessage": "Delete \"{playlistName}\" and all tracks inside it?",
+ "@collectionDeletePlaylistMessage": {
+ "description": "Confirmation message for deleting playlist",
+ "placeholders": {
+ "playlistName": {
+ "type": "String"
+ }
+ }
+ },
+ "collectionPlaylistDeleted": "Playlist deleted",
+ "@collectionPlaylistDeleted": {
+ "description": "Snackbar after deleting playlist"
+ },
+ "collectionPlaylistRenamed": "Playlist renamed",
+ "@collectionPlaylistRenamed": {
+ "description": "Snackbar after renaming playlist"
+ },
+ "collectionWishlistEmptyTitle": "Wishlist is empty",
+ "@collectionWishlistEmptyTitle": {
+ "description": "Wishlist empty state title"
+ },
+ "collectionWishlistEmptySubtitle": "Tap + on tracks to save what you want to download later",
+ "@collectionWishlistEmptySubtitle": {
+ "description": "Wishlist empty state subtitle"
+ },
+ "collectionLovedEmptyTitle": "Loved folder is empty",
+ "@collectionLovedEmptyTitle": {
+ "description": "Loved empty state title"
+ },
+ "collectionLovedEmptySubtitle": "Tap love on tracks to keep your favorites",
+ "@collectionLovedEmptySubtitle": {
+ "description": "Loved empty state subtitle"
+ },
+ "collectionPlaylistEmptyTitle": "Playlist is empty",
+ "@collectionPlaylistEmptyTitle": {
+ "description": "Playlist empty state title"
+ },
+ "collectionPlaylistEmptySubtitle": "Long-press + on any track to add it here",
+ "@collectionPlaylistEmptySubtitle": {
+ "description": "Playlist empty state subtitle"
+ },
+ "collectionRemoveFromPlaylist": "Remove from playlist",
+ "@collectionRemoveFromPlaylist": {
+ "description": "Tooltip for removing track from playlist"
+ },
+ "collectionRemoveFromFolder": "Remove from folder",
+ "@collectionRemoveFromFolder": {
+ "description": "Tooltip for removing track from wishlist/loved folder"
+ },
+ "collectionRemoved": "\"{trackName}\" removed",
+ "@collectionRemoved": {
+ "description": "Snackbar after removing a track from a collection",
+ "placeholders": {
+ "trackName": {
+ "type": "String"
+ }
+ }
+ },
+ "collectionAddedToLoved": "\"{trackName}\" added to Loved",
+ "@collectionAddedToLoved": {
+ "description": "Snackbar after adding track to loved folder",
+ "placeholders": {
+ "trackName": {
+ "type": "String"
+ }
+ }
+ },
+ "collectionRemovedFromLoved": "\"{trackName}\" removed from Loved",
+ "@collectionRemovedFromLoved": {
+ "description": "Snackbar after removing track from loved folder",
+ "placeholders": {
+ "trackName": {
+ "type": "String"
+ }
+ }
+ },
+ "collectionAddedToWishlist": "\"{trackName}\" added to Wishlist",
+ "@collectionAddedToWishlist": {
+ "description": "Snackbar after adding track to wishlist",
+ "placeholders": {
+ "trackName": {
+ "type": "String"
+ }
+ }
+ },
+ "collectionRemovedFromWishlist": "\"{trackName}\" removed from Wishlist",
+ "@collectionRemovedFromWishlist": {
+ "description": "Snackbar after removing track from wishlist",
+ "placeholders": {
+ "trackName": {
+ "type": "String"
+ }
+ }
+ },
+ "trackOptionAddToLoved": "Add to Loved",
+ "@trackOptionAddToLoved": {
+ "description": "Bottom sheet action label - add track to loved folder"
+ },
+ "trackOptionRemoveFromLoved": "Remove from Loved",
+ "@trackOptionRemoveFromLoved": {
+ "description": "Bottom sheet action label - remove track from loved folder"
+ },
+ "trackOptionAddToWishlist": "Add to Wishlist",
+ "@trackOptionAddToWishlist": {
+ "description": "Bottom sheet action label - add track to wishlist"
+ },
+ "trackOptionRemoveFromWishlist": "Remove from Wishlist",
+ "@trackOptionRemoveFromWishlist": {
+ "description": "Bottom sheet action label - remove track from wishlist"
+ },
+ "collectionPlaylistChangeCover": "Change cover image",
+ "@collectionPlaylistChangeCover": {
+ "description": "Bottom sheet action to pick a custom cover image for a playlist"
+ },
+ "collectionPlaylistRemoveCover": "Remove cover image",
+ "@collectionPlaylistRemoveCover": {
+ "description": "Bottom sheet action to remove custom cover image from a playlist"
+ },
+ "selectionShareCount": "Share {count} {count, plural, =1{track} other{tracks}}",
+ "@selectionShareCount": {
+ "description": "Share button text with count in selection mode",
+ "placeholders": {
+ "count": {
+ "type": "int"
+ }
+ }
+ },
+ "selectionShareNoFiles": "No shareable files found",
+ "@selectionShareNoFiles": {
+ "description": "Snackbar when no selected files exist on disk"
+ },
+ "selectionConvertCount": "Convert {count} {count, plural, =1{track} other{tracks}}",
+ "@selectionConvertCount": {
+ "description": "Convert button text with count in selection mode",
+ "placeholders": {
+ "count": {
+ "type": "int"
+ }
+ }
+ },
+ "selectionConvertNoConvertible": "No convertible tracks selected",
+ "@selectionConvertNoConvertible": {
+ "description": "Snackbar when no selected tracks support conversion"
+ },
+ "selectionBatchConvertConfirmTitle": "Batch Convert",
+ "@selectionBatchConvertConfirmTitle": {
+ "description": "Confirmation dialog title for batch conversion"
+ },
+ "selectionBatchConvertConfirmMessage": "Convert {count} {count, plural, =1{track} other{tracks}} to {format} at {bitrate}?\n\nOriginal files will be deleted after conversion.",
+ "@selectionBatchConvertConfirmMessage": {
+ "description": "Confirmation dialog message for batch conversion",
+ "placeholders": {
+ "count": {
+ "type": "int"
+ },
+ "format": {
+ "type": "String"
+ },
+ "bitrate": {
+ "type": "String"
+ }
+ }
+ },
+ "selectionBatchConvertProgress": "Converting {current} of {total}...",
+ "@selectionBatchConvertProgress": {
+ "description": "Snackbar during batch conversion progress",
+ "placeholders": {
+ "current": {
+ "type": "int"
+ },
+ "total": {
+ "type": "int"
+ }
+ }
+ },
+ "selectionBatchConvertSuccess": "Converted {success} of {total} tracks to {format}",
+ "@selectionBatchConvertSuccess": {
+ "description": "Snackbar after batch conversion completes",
+ "placeholders": {
+ "success": {
+ "type": "int"
+ },
+ "total": {
+ "type": "int"
+ },
+ "format": {
+ "type": "String"
+ }
+ }
+ },
"downloadedAlbumDownloadedCount": "{count} 個をダウンロード済み",
"@downloadedAlbumDownloadedCount": {
"description": "Downloaded tracks count badge",
@@ -2800,4 +3102,4 @@
"@downloadUseAlbumArtistForFoldersTrackSubtitle": {
"description": "Subtitle when Track Artist is used for folder naming"
}
-}
+}
\ No newline at end of file
diff --git a/lib/l10n/arb/app_ko.arb b/lib/l10n/arb/app_ko.arb
index 22b488a3..f31eb1b9 100644
--- a/lib/l10n/arb/app_ko.arb
+++ b/lib/l10n/arb/app_ko.arb
@@ -49,7 +49,7 @@
"@historyFilterSingles": {
"description": "Filter chip - show singles only"
},
- "historySearchHint": "Search history...",
+ "historySearchHint": "검색 기록...",
"@historySearchHint": {
"description": "Search bar placeholder in history"
},
@@ -57,43 +57,43 @@
"@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"
},
- "downloadAskQualitySubtitle": "Show quality picker for each download",
+ "downloadAskQualitySubtitle": "다운로드를 할 때마다 품질을 선택하도록 합니다",
"@downloadAskQualitySubtitle": {
"description": "Subtitle for ask quality toggle"
},
- "downloadFilenameFormat": "Filename Format",
+ "downloadFilenameFormat": "파일 이름 형식",
"@downloadFilenameFormat": {
"description": "Setting for output filename pattern"
},
- "downloadFolderOrganization": "Folder Organization",
+ "downloadFolderOrganization": "폴더 분류 형식",
"@downloadFolderOrganization": {
"description": "Setting for folder structure"
},
- "appearanceTitle": "Appearance",
+ "appearanceTitle": "외관",
"@appearanceTitle": {
"description": "Appearance settings page title"
},
@@ -113,11 +113,11 @@
"@appearanceDynamicColor": {
"description": "Material You dynamic colors"
},
- "appearanceDynamicColorSubtitle": "Use colors from your wallpaper",
+ "appearanceDynamicColorSubtitle": "배경 화면을 참고하여 강조 색상이 지정됩니다",
"@appearanceDynamicColorSubtitle": {
"description": "Subtitle for dynamic color"
},
- "appearanceHistoryView": "History View",
+ "appearanceHistoryView": "기록 정렬 방식",
"@appearanceHistoryView": {
"description": "Layout style for history"
},
@@ -129,19 +129,19 @@
"@appearanceHistoryViewGrid": {
"description": "Grid layout option"
},
- "optionsTitle": "Options",
+ "optionsTitle": "옵션",
"@optionsTitle": {
"description": "Options settings page title"
},
- "optionsPrimaryProvider": "Primary Provider",
+ "optionsPrimaryProvider": "기본 제공자",
"@optionsPrimaryProvider": {
"description": "Main search provider setting"
},
- "optionsPrimaryProviderSubtitle": "Service used when searching by track name.",
+ "optionsPrimaryProviderSubtitle": "음반 이름으로 검색할 때 사용되는 서비스",
"@optionsPrimaryProviderSubtitle": {
"description": "Subtitle for primary provider"
},
- "optionsUsingExtension": "Using extension: {extensionName}",
+ "optionsUsingExtension": "확장 기능을 사용: {extensionName}",
"@optionsUsingExtension": {
"description": "Shows active extension name",
"placeholders": {
@@ -150,11 +150,11 @@
}
}
},
- "optionsSwitchBack": "Tap Deezer or Spotify to switch back from extension",
+ "optionsSwitchBack": "Deezer 또는 Spotify를 탭하여 확장 기능에서 다시 전환하세요.",
"@optionsSwitchBack": {
"description": "Hint to switch back to built-in providers"
},
- "optionsAutoFallback": "Auto Fallback",
+ "optionsAutoFallback": "자동 재시도",
"@optionsAutoFallback": {
"description": "Auto-retry with other services"
},
@@ -162,7 +162,7 @@
"@optionsAutoFallbackSubtitle": {
"description": "Subtitle for auto fallback"
},
- "optionsUseExtensionProviders": "Use Extension Providers",
+ "optionsUseExtensionProviders": "확장 기능 사용",
"@optionsUseExtensionProviders": {
"description": "Enable extension download providers"
},
@@ -170,35 +170,35 @@
"@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"
},
- "optionsEmbedLyricsSubtitle": "Embed synced lyrics into FLAC files",
+ "optionsEmbedLyricsSubtitle": "FLAC 파일에 동기화된 가사를 삽입합니다",
"@optionsEmbedLyricsSubtitle": {
"description": "Subtitle for embed lyrics"
},
- "optionsMaxQualityCover": "Max Quality Cover",
+ "optionsMaxQualityCover": "고품질 커버 이미지",
"@optionsMaxQualityCover": {
"description": "Download highest quality album art"
},
- "optionsMaxQualityCoverSubtitle": "Download highest resolution cover art",
+ "optionsMaxQualityCoverSubtitle": "최고 품질의 커버 이미지를 다운로드",
"@optionsMaxQualityCoverSubtitle": {
"description": "Subtitle for max quality cover"
},
- "optionsConcurrentDownloads": "Concurrent Downloads",
+ "optionsConcurrentDownloads": "동시 다운로드",
"@optionsConcurrentDownloads": {
"description": "Number of parallel downloads"
},
- "optionsConcurrentSequential": "Sequential (1 at a time)",
+ "optionsConcurrentSequential": "순차 다운로드 (한 번에 하나)",
"@optionsConcurrentSequential": {
"description": "Download one at a time"
},
- "optionsConcurrentParallel": "{count} parallel downloads",
+ "optionsConcurrentParallel": "{count}개 동시 다운로드",
"@optionsConcurrentParallel": {
"description": "Multiple parallel downloads",
"placeholders": {
@@ -207,63 +207,63 @@
}
}
},
- "optionsConcurrentWarning": "Parallel downloads may trigger rate limiting",
+ "optionsConcurrentWarning": "동시에 다수의 음반을 다운로드하면 속도 제한이 발생할 수 있습니다",
"@optionsConcurrentWarning": {
"description": "Warning about rate limits"
},
- "optionsExtensionStore": "Extension Store",
+ "optionsExtensionStore": "확장 기능 스토어",
"@optionsExtensionStore": {
"description": "Show/hide store tab"
},
- "optionsExtensionStoreSubtitle": "Show Store tab in navigation",
+ "optionsExtensionStoreSubtitle": "탐색 메뉴에 스토어 탭 표시",
"@optionsExtensionStoreSubtitle": {
"description": "Subtitle for extension store toggle"
},
- "optionsCheckUpdates": "Check for Updates",
+ "optionsCheckUpdates": "업데이트 확인",
"@optionsCheckUpdates": {
"description": "Auto update check toggle"
},
- "optionsCheckUpdatesSubtitle": "Notify when new version is available",
+ "optionsCheckUpdatesSubtitle": "새로운 버전이 출시되면 알림",
"@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"
},
- "optionsUpdateChannelWarning": "Preview may contain bugs or incomplete features",
+ "optionsUpdateChannelWarning": "미리보기 버전은 불안정할 수 있습니다",
"@optionsUpdateChannelWarning": {
"description": "Warning about preview channel"
},
- "optionsClearHistory": "Clear Download History",
+ "optionsClearHistory": "다운로드 기록 삭제",
"@optionsClearHistory": {
"description": "Delete all download history"
},
- "optionsClearHistorySubtitle": "Remove all downloaded tracks from history",
+ "optionsClearHistorySubtitle": "기록에서 모든 다운로드 음반을 제거합니다",
"@optionsClearHistorySubtitle": {
"description": "Subtitle for clear history"
},
- "optionsDetailedLogging": "Detailed Logging",
+ "optionsDetailedLogging": "상세 로깅",
"@optionsDetailedLogging": {
"description": "Enable verbose logs for debugging"
},
- "optionsDetailedLoggingOn": "Detailed logs are being recorded",
+ "optionsDetailedLoggingOn": "상세한 로그가 기록되고 있습니다",
"@optionsDetailedLoggingOn": {
"description": "Status when logging enabled"
},
- "optionsDetailedLoggingOff": "Enable for bug reports",
+ "optionsDetailedLoggingOff": "버그 신고를 위한 기능입니다",
"@optionsDetailedLoggingOff": {
"description": "Status when logging disabled"
},
- "optionsSpotifyCredentials": "Spotify Credentials",
+ "optionsSpotifyCredentials": "Spotify 자격 증명",
"@optionsSpotifyCredentials": {
"description": "Spotify API credentials setting"
},
@@ -276,23 +276,23 @@
}
}
},
- "optionsSpotifyCredentialsRequired": "Required - tap to configure",
+ "optionsSpotifyCredentialsRequired": "탭하여 설정",
"@optionsSpotifyCredentialsRequired": {
"description": "Prompt to set up credentials"
},
- "optionsSpotifyWarning": "Spotify requires your own API credentials. Get them free from developer.spotify.com",
+ "optionsSpotifyWarning": "Spotify는 사용자 고유의 API 자격 증명을 요구합니다. developer.spotify.com에서 무료로 발급받으세요",
"@optionsSpotifyWarning": {
"description": "Info about Spotify API requirement"
},
- "optionsSpotifyDeprecationWarning": "Spotify search will be deprecated on March 3, 2026 due to Spotify API changes. Please switch to Deezer.",
+ "optionsSpotifyDeprecationWarning": "Spotify API 변경으로 인해 Spotify 검색 기능은 2026년 3월 3일부터 더 이상 지원되지 않습니다. Deezer로 전환해 주세요",
"@optionsSpotifyDeprecationWarning": {
"description": "Warning about Spotify API deprecation"
},
- "extensionsTitle": "Extensions",
+ "extensionsTitle": "확장 기능",
"@extensionsTitle": {
"description": "Extensions page title"
},
- "extensionsDisabled": "Disabled",
+ "extensionsDisabled": "비활성화",
"@extensionsDisabled": {
"description": "Extension status - inactive"
},
@@ -314,59 +314,59 @@
}
}
},
- "extensionsUninstall": "Uninstall",
+ "extensionsUninstall": "삭제",
"@extensionsUninstall": {
"description": "Uninstall extension button"
},
- "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"
},
- "aboutOriginalCreator": "Creator of the original SpotiFLAC",
+ "aboutOriginalCreator": "오리지널 SpotiFLAC 제작자",
"@aboutOriginalCreator": {
"description": "Role description for original creator"
},
- "aboutLogoArtist": "The talented artist who created our beautiful app logo!",
+ "aboutLogoArtist": "아름다운 로고를 만들어주신 재능 있는 아티스트!",
"@aboutLogoArtist": {
"description": "Role description for logo artist"
},
- "aboutTranslators": "Translators",
+ "aboutTranslators": "번역가들",
"@aboutTranslators": {
"description": "Section for translators"
},
- "aboutSpecialThanks": "Special Thanks",
+ "aboutSpecialThanks": "특별 감사",
"@aboutSpecialThanks": {
"description": "Section for special thanks"
},
- "aboutLinks": "Links",
+ "aboutLinks": "바로가기",
"@aboutLinks": {
"description": "Section for external links"
},
@@ -374,23 +374,23 @@
"@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": "문제 신고",
"@aboutReportIssue": {
"description": "Link to report bugs"
},
- "aboutReportIssueSubtitle": "Report any problems you encounter",
+ "aboutReportIssueSubtitle": "발생하는 모든 문제를 신고하여 주세요.",
"@aboutReportIssueSubtitle": {
"description": "Subtitle for report issue"
},
- "aboutFeatureRequest": "Feature request",
+ "aboutFeatureRequest": "기능 요청",
"@aboutFeatureRequest": {
"description": "Link to suggest features"
},
- "aboutFeatureRequestSubtitle": "Suggest new features for the app",
+ "aboutFeatureRequestSubtitle": "앱의 새로운 기능을 제안하여 주세요.",
"@aboutFeatureRequestSubtitle": {
"description": "Subtitle for feature request"
},
@@ -398,7 +398,7 @@
"@aboutTelegramChannel": {
"description": "Link to Telegram channel"
},
- "aboutTelegramChannelSubtitle": "Announcements and updates",
+ "aboutTelegramChannelSubtitle": "공지 및 업데이트 안내",
"@aboutTelegramChannelSubtitle": {
"description": "Subtitle for Telegram channel"
},
@@ -406,11 +406,11 @@
"@aboutTelegramChat": {
"description": "Link to Telegram chat group"
},
- "aboutTelegramChatSubtitle": "Chat with other users",
+ "aboutTelegramChatSubtitle": "다른 이용자와 소통",
"@aboutTelegramChatSubtitle": {
"description": "Subtitle for Telegram chat"
},
- "aboutSocial": "Social",
+ "aboutSocial": "소셜",
"@aboutSocial": {
"description": "Section for social links"
},
@@ -422,15 +422,15 @@
"@aboutVersion": {
"description": "Version info label"
},
- "aboutBinimumDesc": "The creator of QQDL & HiFi API. Without this API, Tidal downloads wouldn't exist!",
+ "aboutBinimumDesc": "QQDL 및 HiFi API 개발자입니다. 이 API가 없었다면 Tidal 다운로드는 불가능했을 것입니다!",
"@aboutBinimumDesc": {
"description": "Credit description for binimum"
},
- "aboutSachinsenalDesc": "The original HiFi project creator. The foundation of Tidal integration!",
+ "aboutSachinsenalDesc": "최초의 하이파이 프로젝트 창시자. 타이달 연동의 기반을 마련한 사람!",
"@aboutSachinsenalDesc": {
"description": "Credit description for sachinsenal0x64"
},
- "aboutSjdonadoDesc": "Creator of I Don't Have Spotify (IDHS). The fallback link resolver that saves the day!",
+ "aboutSjdonadoDesc": "I Don't Have Spotify(IDHS) 개발자입니다. 위급 상황 발생 시 해결해 주는 대체 링크 해결 도구를 만들었습니다!",
"@aboutSjdonadoDesc": {
"description": "Credit description for sjdonado"
},
@@ -438,7 +438,7 @@
"@aboutDabMusic": {
"description": "Name of Qobuz API service - DO NOT TRANSLATE"
},
- "aboutDabMusicDesc": "The best Qobuz streaming API. Hi-Res downloads wouldn't be possible without this!",
+ "aboutDabMusicDesc": "최고의 Qobuz 스트리밍 API입니다. 이 API가 없었다면 고해상도 다운로드는 불가능했을 겁니다!",
"@aboutDabMusicDesc": {
"description": "Credit for DAB Music API"
},
@@ -446,31 +446,31 @@
"@aboutSpotiSaver": {
"description": "Name of SpotiSaver API service - DO NOT TRANSLATE"
},
- "aboutSpotiSaverDesc": "Tidal Hi-Res FLAC streaming endpoints. A key piece of the lossless puzzle!",
+ "aboutSpotiSaverDesc": "Tidal Hi-Res FLAC 스트리밍 엔드포인트. 무손실 음원 재생의 핵심 요소!",
"@aboutSpotiSaverDesc": {
"description": "Credit for SpotiSaver API"
},
- "aboutAppDescription": "Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.",
+ "aboutAppDescription": "Tidal, Qobuz, Amazon Music에서 Spotify 트랙을 무손실 음질로 다운로드하세요.",
"@aboutAppDescription": {
"description": "App description in header card"
},
- "artistAlbums": "Albums",
+ "artistAlbums": "앨범",
"@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"
},
- "artistPopular": "Popular",
+ "artistPopular": "인기순",
"@artistPopular": {
"description": "Section header for popular/top tracks"
},
- "artistMonthlyListeners": "{count} monthly listeners",
+ "artistMonthlyListeners": "월간 청취자: {count}",
"@artistMonthlyListeners": {
"description": "Monthly listener count display",
"placeholders": {
@@ -480,47 +480,47 @@
}
}
},
- "trackMetadataService": "Service",
+ "trackMetadataService": "제공업체",
"@trackMetadataService": {
"description": "Metadata field - download service used"
},
- "trackMetadataPlay": "Play",
+ "trackMetadataPlay": "재생",
"@trackMetadataPlay": {
"description": "Action button - play track"
},
- "trackMetadataShare": "Share",
+ "trackMetadataShare": "공유",
"@trackMetadataShare": {
"description": "Action button - share track"
},
- "trackMetadataDelete": "Delete",
+ "trackMetadataDelete": "삭제",
"@trackMetadataDelete": {
"description": "Action button - delete track"
},
- "setupGrantPermission": "Grant Permission",
+ "setupGrantPermission": "권한을 제공해 주세요.",
"@setupGrantPermission": {
"description": "Button to request permission"
},
- "setupSkip": "Skip for now",
+ "setupSkip": "다음에 할래요",
"@setupSkip": {
"description": "Skip current step button"
},
- "setupStorageAccessRequired": "Storage Access Required",
+ "setupStorageAccessRequired": "스토리지 접근 권한 필요",
"@setupStorageAccessRequired": {
"description": "Title when storage access needed"
},
- "setupStorageAccessMessageAndroid11": "Android 11+ requires \"All files access\" permission to save files to your chosen download folder.",
+ "setupStorageAccessMessageAndroid11": "Android 11 이상 버전에서는 선택한 다운로드 폴더에 파일을 저장하려면 \"모든 파일 접근\" 권한이 필요합니다.",
"@setupStorageAccessMessageAndroid11": {
"description": "Android 11+ specific explanation"
},
- "setupOpenSettings": "Open Settings",
+ "setupOpenSettings": "설정으로 이동",
"@setupOpenSettings": {
"description": "Button to open system settings"
},
- "setupPermissionDeniedMessage": "Permission denied. Please grant all permissions to continue.",
+ "setupPermissionDeniedMessage": "권한이 거부되었습니다. 계속하려면 모든 권한을 허용해 주세요.",
"@setupPermissionDeniedMessage": {
"description": "Error when permission denied"
},
- "setupPermissionRequired": "{permissionType} Permission Required",
+ "setupPermissionRequired": "{permissionType} 권한 필요",
"@setupPermissionRequired": {
"description": "Generic permission required title",
"placeholders": {
@@ -530,7 +530,7 @@
}
}
},
- "setupPermissionRequiredMessage": "{permissionType} permission is required for the best experience. You can change this later in Settings.",
+ "setupPermissionRequiredMessage": "최상의 사용 경험을 위해 {permissionType} 권한이 필요합니다. 설정에서 나중에 변경할 수 있습니다.",
"@setupPermissionRequiredMessage": {
"description": "Generic permission required message",
"placeholders": {
@@ -539,175 +539,175 @@
}
}
},
- "setupUseDefaultFolder": "Use Default Folder?",
+ "setupUseDefaultFolder": "기본 폴더를 사용하시겠습니까?",
"@setupUseDefaultFolder": {
"description": "Dialog title for default folder"
},
- "setupNoFolderSelected": "No folder selected. Would you like to use the default Music folder?",
+ "setupNoFolderSelected": "선택된 폴더가 없습니다. 기본 음악 폴더를 사용하시겠습니까?",
"@setupNoFolderSelected": {
"description": "Prompt when no folder selected"
},
- "setupUseDefault": "Use Default",
+ "setupUseDefault": "기본값 사용",
"@setupUseDefault": {
"description": "Button to use default folder"
},
- "setupDownloadLocationTitle": "Download Location",
+ "setupDownloadLocationTitle": "다운로드 경로",
"@setupDownloadLocationTitle": {
"description": "Download location dialog title"
},
- "setupDownloadLocationIosMessage": "On iOS, downloads are saved to the app's Documents folder. You can access them via the Files app.",
+ "setupDownloadLocationIosMessage": "iOS에서는 다운로드한 파일이 앱의 문서 폴더에 저장됩니다. 파일 앱을 통해 해당 파일에 접근할 수 있습니다.",
"@setupDownloadLocationIosMessage": {
"description": "iOS-specific folder info"
},
- "setupAppDocumentsFolder": "App Documents Folder",
+ "setupAppDocumentsFolder": "앱 문서 폴더",
"@setupAppDocumentsFolder": {
"description": "iOS documents folder option"
},
- "setupAppDocumentsFolderSubtitle": "Recommended - accessible via Files app",
+ "setupAppDocumentsFolderSubtitle": "권장 사항 - 파일 앱을 통해 접근 가능",
"@setupAppDocumentsFolderSubtitle": {
"description": "Subtitle for documents folder"
},
- "setupChooseFromFiles": "Choose from Files",
+ "setupChooseFromFiles": "파일 탐색기에서 선택",
"@setupChooseFromFiles": {
"description": "iOS file picker option"
},
- "setupChooseFromFilesSubtitle": "Select iCloud or other location",
+ "setupChooseFromFilesSubtitle": "iCloud 또는 다른 위치를 선택하세요",
"@setupChooseFromFilesSubtitle": {
"description": "Subtitle for file picker"
},
- "setupIosEmptyFolderWarning": "iOS limitation: Empty folders cannot be selected. Choose a folder with at least one file.",
+ "setupIosEmptyFolderWarning": "iOS 제한 사항: 빈 폴더는 선택할 수 없습니다. 파일이 하나 이상 있는 폴더를 선택하세요.",
"@setupIosEmptyFolderWarning": {
"description": "iOS folder selection warning"
},
- "setupIcloudNotSupported": "iCloud Drive is not supported. Please use the app Documents folder.",
+ "setupIcloudNotSupported": "iCloud Drive는 지원되지 않습니다. 앱의 문서 폴더를 사용해 주세요.",
"@setupIcloudNotSupported": {
"description": "Error when user selects iCloud Drive on iOS"
},
- "setupDownloadInFlac": "Download Spotify tracks in FLAC",
+ "setupDownloadInFlac": "Spotify 음악을 FLAC 형식으로 다운로드하세요.",
"@setupDownloadInFlac": {
"description": "App tagline in setup"
},
- "setupStorageGranted": "Storage Permission Granted!",
+ "setupStorageGranted": "저장소 접근 권한이 부여되었습니다!",
"@setupStorageGranted": {
"description": "Success message for storage permission"
},
- "setupStorageRequired": "Storage Permission Required",
+ "setupStorageRequired": "저장소 접근 권한이 필요합니다.",
"@setupStorageRequired": {
"description": "Title when storage permission needed"
},
- "setupStorageDescription": "SpotiFLAC needs storage permission to save your downloaded music files.",
+ "setupStorageDescription": "SpotiFLAC은 다운로드한 음악 파일을 저장하기 위해 저장소 접근 권한이 필요합니다.",
"@setupStorageDescription": {
"description": "Explanation for storage permission"
},
- "setupNotificationGranted": "Notification Permission Granted!",
+ "setupNotificationGranted": "알림 권한이 부여되었습니다!",
"@setupNotificationGranted": {
"description": "Success message for notification permission"
},
- "setupNotificationEnable": "Enable Notifications",
+ "setupNotificationEnable": "알림 활성화",
"@setupNotificationEnable": {
"description": "Button to enable notifications"
},
- "setupFolderChoose": "Choose Download Folder",
+ "setupFolderChoose": "다운로드 폴더를 선택하세요",
"@setupFolderChoose": {
"description": "Button to choose folder"
},
- "setupFolderDescription": "Select a folder where your downloaded music will be saved.",
+ "setupFolderDescription": "다운로드한 음악 파일이 저장될 폴더를 선택하세요.",
"@setupFolderDescription": {
"description": "Explanation for folder selection"
},
- "setupSelectFolder": "Select Folder",
+ "setupSelectFolder": "폴더 선택",
"@setupSelectFolder": {
"description": "Button to select folder"
},
- "setupEnableNotifications": "Enable Notifications",
+ "setupEnableNotifications": "알림 활성화",
"@setupEnableNotifications": {
"description": "Button to enable notifications"
},
- "setupNotificationBackgroundDescription": "Get notified about download progress and completion. This helps you track downloads when the app is in background.",
+ "setupNotificationBackgroundDescription": "알림으로 다운로드 진행 상황을 확인하세요. 앱이 백그라운드에서 실행 중일 때 다운로드 상태와 완료 여부를 확인할 수 있습니다.",
"@setupNotificationBackgroundDescription": {
"description": "Detailed notification explanation"
},
- "setupSkipForNow": "Skip for now",
+ "setupSkipForNow": "다음에 할래요.",
"@setupSkipForNow": {
"description": "Skip button text"
},
- "setupNext": "Next",
+ "setupNext": "다음",
"@setupNext": {
"description": "Next button text"
},
- "setupGetStarted": "Get Started",
+ "setupGetStarted": "시작하기",
"@setupGetStarted": {
"description": "Final setup button"
},
- "setupAllowAccessToManageFiles": "Please enable \"Allow access to manage all files\" in the next screen.",
+ "setupAllowAccessToManageFiles": "다음 화면에서 \"모든 파일 관리 권한 허용\"을 활성화해 주세요.",
"@setupAllowAccessToManageFiles": {
"description": "Instruction for file access permission"
},
- "dialogCancel": "Cancel",
+ "dialogCancel": "취소",
"@dialogCancel": {
"description": "Dialog button - cancel action"
},
- "dialogSave": "Save",
+ "dialogSave": "저장",
"@dialogSave": {
"description": "Dialog button - save changes"
},
- "dialogDelete": "Delete",
+ "dialogDelete": "삭제",
"@dialogDelete": {
"description": "Dialog button - delete item"
},
- "dialogRetry": "Retry",
+ "dialogRetry": "재시도",
"@dialogRetry": {
"description": "Dialog button - retry action"
},
- "dialogClear": "Clear",
+ "dialogClear": "지우기",
"@dialogClear": {
"description": "Dialog button - clear items"
},
- "dialogDone": "Done",
+ "dialogDone": "완료",
"@dialogDone": {
"description": "Dialog button - action completed"
},
- "dialogImport": "Import",
+ "dialogImport": "불러오기",
"@dialogImport": {
"description": "Dialog button - import data"
},
- "dialogDiscard": "Discard",
+ "dialogDiscard": "취소",
"@dialogDiscard": {
"description": "Dialog button - discard changes"
},
- "dialogRemove": "Remove",
+ "dialogRemove": "제거",
"@dialogRemove": {
"description": "Dialog button - remove item"
},
- "dialogUninstall": "Uninstall",
+ "dialogUninstall": "삭제",
"@dialogUninstall": {
"description": "Dialog button - uninstall extension"
},
- "dialogDiscardChanges": "Discard Changes?",
+ "dialogDiscardChanges": "변경사항 취소",
"@dialogDiscardChanges": {
"description": "Dialog title - unsaved changes warning"
},
- "dialogUnsavedChanges": "You have unsaved changes. Do you want to discard them?",
+ "dialogUnsavedChanges": "저장되지 않은 변경 사항이 있습니다. 삭제하시겠습니까?",
"@dialogUnsavedChanges": {
"description": "Dialog message - unsaved changes"
},
- "dialogClearAll": "Clear All",
+ "dialogClearAll": "모두 제거:",
"@dialogClearAll": {
"description": "Dialog title - clear all items"
},
- "dialogRemoveExtension": "Remove Extension",
+ "dialogRemoveExtension": "확장 프로그램 제거",
"@dialogRemoveExtension": {
"description": "Dialog title - uninstall extension"
},
- "dialogRemoveExtensionMessage": "Are you sure you want to remove this extension? This cannot be undone.",
+ "dialogRemoveExtensionMessage": "이 확장 프로그램을 정말로 제거하시겠습니까? 이 작업은 되돌릴 수 없습니다.",
"@dialogRemoveExtensionMessage": {
"description": "Dialog message - uninstall confirmation"
},
- "dialogUninstallExtension": "Uninstall Extension?",
+ "dialogUninstallExtension": "확장 프로그램을 제거하시겠습니까?",
"@dialogUninstallExtension": {
"description": "Dialog title - uninstall extension"
},
- "dialogUninstallExtensionMessage": "Are you sure you want to remove {extensionName}?",
+ "dialogUninstallExtensionMessage": "{extensionName}을 정말로 삭제하시겠습니까?",
"@dialogUninstallExtensionMessage": {
"description": "Dialog message - uninstall specific extension",
"placeholders": {
@@ -716,19 +716,19 @@
}
}
},
- "dialogClearHistoryTitle": "Clear History",
+ "dialogClearHistoryTitle": "기록 삭제",
"@dialogClearHistoryTitle": {
"description": "Dialog title - clear download history"
},
- "dialogClearHistoryMessage": "Are you sure you want to clear all download history? This cannot be undone.",
+ "dialogClearHistoryMessage": "다운로드 기록을 모두 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.",
"@dialogClearHistoryMessage": {
"description": "Dialog message - clear history confirmation"
},
- "dialogDeleteSelectedTitle": "Delete Selected",
+ "dialogDeleteSelectedTitle": "선택한 항목 삭제",
"@dialogDeleteSelectedTitle": {
"description": "Dialog title - delete selected items"
},
- "dialogDeleteSelectedMessage": "Delete {count} {count, plural, =1{track} other{tracks}} from history?\n\nThis will also delete the files from storage.",
+ "dialogDeleteSelectedMessage": "기록에서 {count} {count, plural, =1{track} other{tracks}}를 삭제하시겠습니까?",
"@dialogDeleteSelectedMessage": {
"description": "Dialog message - delete selected tracks",
"placeholders": {
@@ -737,12 +737,12 @@
}
}
},
- "dialogImportPlaylistTitle": "Import Playlist",
+ "dialogImportPlaylistTitle": "재생 목록 가져오기",
"@dialogImportPlaylistTitle": {
"description": "Dialog title - import CSV playlist"
},
- "dialogImportPlaylistMessage": "Found {count} tracks in CSV. Add them to download queue?",
- "csvImportTracks": "{count} tracks from CSV",
+ "dialogImportPlaylistMessage": "CSV 파일에서 {count}개의 트랙을 찾았습니다. 다운로드 대기열에 추가하시겠습니까?",
+ "csvImportTracks": "CSV 파일의 트랙: {count}",
"@csvImportTracks": {
"description": "Label shown in quality picker for CSV import",
"placeholders": {
@@ -759,7 +759,7 @@
}
}
},
- "snackbarAddedToQueue": "Added \"{trackName}\" to queue",
+ "snackbarAddedToQueue": "\"{trackName}\"(을)를 대기열에 추가했습니다.",
"@snackbarAddedToQueue": {
"description": "Snackbar - track added to download queue",
"placeholders": {
@@ -768,7 +768,7 @@
}
}
},
- "snackbarAddedTracksToQueue": "Added {count} tracks to queue",
+ "snackbarAddedTracksToQueue": "대기열에 {count}개의 트랙을 추가했습니다.",
"@snackbarAddedTracksToQueue": {
"description": "Snackbar - multiple tracks added to queue",
"placeholders": {
@@ -777,7 +777,7 @@
}
}
},
- "snackbarAlreadyDownloaded": "\"{trackName}\" already downloaded",
+ "snackbarAlreadyDownloaded": "\"{trackName}\"(은)는 이미 다운로드되었습니다.",
"@snackbarAlreadyDownloaded": {
"description": "Snackbar - track already exists",
"placeholders": {
@@ -786,7 +786,7 @@
}
}
},
- "snackbarAlreadyInLibrary": "\"{trackName}\" already exists in your library",
+ "snackbarAlreadyInLibrary": "라이브러리에 \"{trackName}\"(은)는 이미 존재합니다.",
"@snackbarAlreadyInLibrary": {
"description": "Snackbar - track already exists in local library",
"placeholders": {
@@ -795,19 +795,19 @@
}
}
},
- "snackbarHistoryCleared": "History cleared",
+ "snackbarHistoryCleared": "기록 삭제됨",
"@snackbarHistoryCleared": {
"description": "Snackbar - history deleted"
},
- "snackbarCredentialsSaved": "Credentials saved",
+ "snackbarCredentialsSaved": "자격 증명이 저장되었습니다.",
"@snackbarCredentialsSaved": {
"description": "Snackbar - Spotify credentials saved"
},
- "snackbarCredentialsCleared": "Credentials cleared",
+ "snackbarCredentialsCleared": "자격 증명이 제거되었습니다.",
"@snackbarCredentialsCleared": {
"description": "Snackbar - Spotify credentials removed"
},
- "snackbarDeletedTracks": "Deleted {count} {count, plural, =1{track} other{tracks}}",
+ "snackbarDeletedTracks": "{count}{count, plural,=1{track}other{tracks}} 제거됨",
"@snackbarDeletedTracks": {
"description": "Snackbar - tracks deleted",
"placeholders": {
@@ -816,7 +816,7 @@
}
}
},
- "snackbarCannotOpenFile": "Cannot open file: {error}",
+ "snackbarCannotOpenFile": "파일을 열 수 없습니다: {error}",
"@snackbarCannotOpenFile": {
"description": "Snackbar - file open error",
"placeholders": {
@@ -825,7 +825,7 @@
}
}
},
- "snackbarFillAllFields": "Please fill all fields",
+ "snackbarFillAllFields": "모든 항목을 입력해 주세요.",
"@snackbarFillAllFields": {
"description": "Snackbar - validation error"
},
@@ -833,7 +833,7 @@
"@snackbarViewQueue": {
"description": "Snackbar action - view download queue"
},
- "snackbarUrlCopied": "{platform} URL copied to clipboard",
+ "snackbarUrlCopied": "{platform} 링크가 클립보드에 저장됨",
"@snackbarUrlCopied": {
"description": "Snackbar - URL copied",
"placeholders": {
@@ -843,23 +843,23 @@
}
}
},
- "snackbarFileNotFound": "File not found",
+ "snackbarFileNotFound": "파일을 찾을 수 없음",
"@snackbarFileNotFound": {
"description": "Snackbar - file doesn't exist"
},
- "snackbarSelectExtFile": "Please select a .spotiflac-ext file",
+ "snackbarSelectExtFile": ".spotiflac-ext 확장자 파일을 선택",
"@snackbarSelectExtFile": {
"description": "Snackbar - wrong file type selected"
},
- "snackbarProviderPrioritySaved": "Provider priority saved",
+ "snackbarProviderPrioritySaved": "제공자 우선순위 저장됨",
"@snackbarProviderPrioritySaved": {
"description": "Snackbar - provider order saved"
},
- "snackbarMetadataProviderSaved": "Metadata provider priority saved",
+ "snackbarMetadataProviderSaved": "메타데이터 제공자 우선순위 저장됨",
"@snackbarMetadataProviderSaved": {
"description": "Snackbar - metadata provider order saved"
},
- "snackbarExtensionInstalled": "{extensionName} installed.",
+ "snackbarExtensionInstalled": "{extensionName}(이)가 설치됨",
"@snackbarExtensionInstalled": {
"description": "Snackbar - extension installed successfully",
"placeholders": {
@@ -868,7 +868,7 @@
}
}
},
- "snackbarExtensionUpdated": "{extensionName} updated.",
+ "snackbarExtensionUpdated": "{extensionName}(이)가 설치됨.",
"@snackbarExtensionUpdated": {
"description": "Snackbar - extension updated successfully",
"placeholders": {
@@ -877,11 +877,11 @@
}
}
},
- "snackbarFailedToInstall": "Failed to install extension",
+ "snackbarFailedToInstall": "확장 프로그램 설치 실패",
"@snackbarFailedToInstall": {
"description": "Snackbar - extension install error"
},
- "snackbarFailedToUpdate": "Failed to update extension",
+ "snackbarFailedToUpdate": "확장 프로그램 업데이트 실패",
"@snackbarFailedToUpdate": {
"description": "Snackbar - extension update error"
},
@@ -889,15 +889,15 @@
"@errorRateLimited": {
"description": "Error title - too many requests"
},
- "errorRateLimitedMessage": "Too many requests. Please wait a moment before searching again.",
+ "errorRateLimitedMessage": "요청이 너무 많습니다. 잠시 후 다시 검색해 주세요.",
"@errorRateLimitedMessage": {
"description": "Error message - rate limit explanation"
},
- "errorNoTracksFound": "No tracks found",
+ "errorNoTracksFound": "트랙을 찾을 수 없습니다",
"@errorNoTracksFound": {
"description": "Error - search returned no results"
},
- "errorMissingExtensionSource": "Cannot load {item}: missing extension source",
+ "errorMissingExtensionSource": "확장 소스가 누락되어, {item}(을)를 로드할 수 없습니다",
"@errorMissingExtensionSource": {
"description": "Error - extension source not available",
"placeholders": {
@@ -906,35 +906,35 @@
}
}
},
- "actionPause": "Pause",
+ "actionPause": "멈추기",
"@actionPause": {
"description": "Action button - pause download"
},
- "actionResume": "Resume",
+ "actionResume": "재시작",
"@actionResume": {
"description": "Action button - resume download"
},
- "actionCancel": "Cancel",
+ "actionCancel": "취소",
"@actionCancel": {
"description": "Action button - cancel operation"
},
- "actionSelectAll": "Select All",
+ "actionSelectAll": "모두 선택",
"@actionSelectAll": {
"description": "Action button - select all items"
},
- "actionDeselect": "Deselect",
+ "actionDeselect": "선택 해제",
"@actionDeselect": {
"description": "Action button - deselect all"
},
- "actionRemoveCredentials": "Remove Credentials",
+ "actionRemoveCredentials": "자격 증명 제거",
"@actionRemoveCredentials": {
"description": "Action button - delete Spotify credentials"
},
- "actionSaveCredentials": "Save Credentials",
+ "actionSaveCredentials": "자격 증명 저장",
"@actionSaveCredentials": {
"description": "Action button - save Spotify credentials"
},
- "selectionSelected": "{count} selected",
+ "selectionSelected": "{count}개 선택됨",
"@selectionSelected": {
"description": "Selection count indicator",
"placeholders": {
@@ -943,15 +943,15 @@
}
}
},
- "selectionAllSelected": "All tracks selected",
+ "selectionAllSelected": "모든 트랙 선택됨",
"@selectionAllSelected": {
"description": "Status - all items selected"
},
- "selectionSelectToDelete": "Select tracks to delete",
+ "selectionSelectToDelete": "삭제할 트랙을 선택",
"@selectionSelectToDelete": {
"description": "Placeholder when nothing selected"
},
- "progressFetchingMetadata": "Fetching metadata... {current}/{total}",
+ "progressFetchingMetadata": "메타데이터 가져오는 중... {current}/{total}",
"@progressFetchingMetadata": {
"description": "Progress indicator - loading track info",
"placeholders": {
@@ -963,35 +963,43 @@
}
}
},
- "progressReadingCsv": "Reading CSV...",
+ "progressReadingCsv": "CSV 파일을 읽는 중...",
"@progressReadingCsv": {
"description": "Progress indicator - parsing CSV file"
},
- "searchSongs": "Songs",
+ "searchSongs": "곡들",
"@searchSongs": {
"description": "Search result category - songs"
},
- "searchArtists": "Artists",
+ "searchArtists": "아티스트들",
"@searchArtists": {
"description": "Search result category - artists"
},
- "searchAlbums": "Albums",
+ "searchAlbums": "앨범들",
"@searchAlbums": {
"description": "Search result category - albums"
},
- "searchPlaylists": "Playlists",
+ "searchPlaylists": "재생목록들",
"@searchPlaylists": {
"description": "Search result category - playlists"
},
- "tooltipPlay": "Play",
+ "tooltipPlay": "재생",
"@tooltipPlay": {
"description": "Tooltip - play button"
},
- "filenameFormat": "Filename Format",
+ "filenameFormat": "",
"@filenameFormat": {
"description": "Setting title - filename pattern"
},
- "folderOrganizationNone": "No organization",
+ "filenameShowAdvancedTags": "고급 태그 표시",
+ "@filenameShowAdvancedTags": {
+ "description": "Toggle label for showing advanced filename tags"
+ },
+ "filenameShowAdvancedTagsDescription": "트랙 패딩 및 날짜 패턴에 대한 서식 있는 태그를 활성화합니다.",
+ "@filenameShowAdvancedTagsDescription": {
+ "description": "Description for advanced filename tag toggle"
+ },
+ "folderOrganizationNone": "정리하지 않음",
"@folderOrganizationNone": {
"description": "Folder option - flat structure"
},
@@ -1139,31 +1147,31 @@
"@logShareLogs": {
"description": "Share button tooltip"
},
- "logClearLogs": "Clear logs",
+ "logClearLogs": "로그 제거",
"@logClearLogs": {
"description": "Clear button tooltip"
},
- "logClearLogsTitle": "Clear Logs",
+ "logClearLogsTitle": "로그 제거",
"@logClearLogsTitle": {
"description": "Clear logs dialog title"
},
- "logClearLogsMessage": "Are you sure you want to clear all logs?",
+ "logClearLogsMessage": "모든 로그를 삭제하시겠습니까?",
"@logClearLogsMessage": {
"description": "Clear logs confirmation message"
},
- "logFilterBySeverity": "Filter logs by severity",
+ "logFilterBySeverity": "심각성에 따라 로그 분류",
"@logFilterBySeverity": {
"description": "Filter dialog title"
},
- "logNoLogsYet": "No logs yet",
+ "logNoLogsYet": "어떠한 로그도 없음",
"@logNoLogsYet": {
"description": "Empty state title"
},
- "logNoLogsYetSubtitle": "Logs will appear here as you use the app",
+ "logNoLogsYetSubtitle": "앱을 사용하는 동안 로그가 여기에 표시됩니다.",
"@logNoLogsYetSubtitle": {
"description": "Empty state subtitle"
},
- "logEntriesFiltered": "Entries ({count} filtered)",
+ "logEntriesFiltered": "({count} filtered)개 항목 필터링",
"@logEntriesFiltered": {
"description": "Log count with filter active",
"placeholders": {
@@ -1172,7 +1180,7 @@
}
}
},
- "logEntries": "Entries ({count})",
+ "logEntries": "항목 수: ({count})",
"@logEntries": {
"description": "Total log count",
"placeholders": {
@@ -1181,11 +1189,11 @@
}
}
},
- "credentialsTitle": "Spotify Credentials",
+ "credentialsTitle": "Spotify 자격 증명",
"@credentialsTitle": {
"description": "Credentials dialog title"
},
- "credentialsDescription": "Enter your Client ID and Secret to use your own Spotify application quota.",
+ "credentialsDescription": "Spotify 애플리케이션 할당량을 사용하려면 클라이언트 ID와 비밀키를 입력하세요.",
"@credentialsDescription": {
"description": "Credentials dialog explanation"
},
@@ -1193,43 +1201,43 @@
"@credentialsClientId": {
"description": "Client ID field label - DO NOT TRANSLATE"
},
- "credentialsClientIdHint": "Paste Client ID",
+ "credentialsClientIdHint": "Client ID를 붙여넣으세요",
"@credentialsClientIdHint": {
"description": "Client ID placeholder"
},
- "credentialsClientSecret": "Client Secret",
+ "credentialsClientSecret": "비밀키",
"@credentialsClientSecret": {
"description": "Client Secret field label - DO NOT TRANSLATE"
},
- "credentialsClientSecretHint": "Paste Client Secret",
+ "credentialsClientSecretHint": "비밀키를 붙여넣으세요",
"@credentialsClientSecretHint": {
"description": "Client Secret placeholder"
},
- "channelStable": "Stable",
+ "channelStable": "안정",
"@channelStable": {
"description": "Update channel - stable releases"
},
- "channelPreview": "Preview",
+ "channelPreview": "베타",
"@channelPreview": {
"description": "Update channel - beta/preview releases"
},
- "sectionSearchSource": "Search Source",
+ "sectionSearchSource": "검색 소스",
"@sectionSearchSource": {
"description": "Settings section header"
},
- "sectionDownload": "Download",
+ "sectionDownload": "다운로드",
"@sectionDownload": {
"description": "Settings section header"
},
- "sectionPerformance": "Performance",
+ "sectionPerformance": "성능",
"@sectionPerformance": {
"description": "Settings section header"
},
- "sectionApp": "App",
+ "sectionApp": "앱",
"@sectionApp": {
"description": "Settings section header"
},
- "sectionData": "Data",
+ "sectionData": "데이터",
"@sectionData": {
"description": "Settings section header"
},
@@ -1237,55 +1245,55 @@
"@sectionDebug": {
"description": "Settings section header"
},
- "sectionService": "Service",
+ "sectionService": "서비스",
"@sectionService": {
"description": "Settings section header"
},
- "sectionAudioQuality": "Audio Quality",
+ "sectionAudioQuality": "오디오 품질",
"@sectionAudioQuality": {
"description": "Settings section header"
},
- "sectionFileSettings": "File Settings",
+ "sectionFileSettings": "파일 설정",
"@sectionFileSettings": {
"description": "Settings section header"
},
- "sectionLyrics": "Lyrics",
+ "sectionLyrics": "가사",
"@sectionLyrics": {
"description": "Settings section header"
},
- "lyricsMode": "Lyrics Mode",
+ "lyricsMode": "가사 설정",
"@lyricsMode": {
"description": "Setting - how to save lyrics"
},
- "lyricsModeDescription": "Choose how lyrics are saved with your downloads",
+ "lyricsModeDescription": "다운로드한 파일에 가사를 저장하는 방법을 선택하세요.",
"@lyricsModeDescription": {
"description": "Lyrics mode picker description"
},
- "lyricsModeEmbed": "Embed in file",
+ "lyricsModeEmbed": "파일에 포함",
"@lyricsModeEmbed": {
"description": "Lyrics mode option - embed in audio file"
},
- "lyricsModeEmbedSubtitle": "Lyrics stored inside FLAC metadata",
+ "lyricsModeEmbedSubtitle": "FLAC 메타데이터 내에 저장됩니다.",
"@lyricsModeEmbedSubtitle": {
"description": "Subtitle for embed option"
},
- "lyricsModeExternal": "External .lrc file",
+ "lyricsModeExternal": "외부 .lrc 파일",
"@lyricsModeExternal": {
"description": "Lyrics mode option - separate LRC file"
},
- "lyricsModeExternalSubtitle": "Separate .lrc file for players like Samsung Music",
+ "lyricsModeExternalSubtitle": "삼성 뮤직과 같은 플레이어용 별도 .lrc 파일",
"@lyricsModeExternalSubtitle": {
"description": "Subtitle for external option"
},
- "lyricsModeBoth": "Both",
+ "lyricsModeBoth": "둘 다",
"@lyricsModeBoth": {
"description": "Lyrics mode option - embed and external"
},
- "lyricsModeBothSubtitle": "Embed and save .lrc file",
+ "lyricsModeBothSubtitle": ".lrc 파일을 삽입하고 저장합니다.",
"@lyricsModeBothSubtitle": {
"description": "Subtitle for both option"
},
- "sectionColor": "Color",
+ "sectionColor": "색상",
"@sectionColor": {
"description": "Settings section header"
},
@@ -1749,6 +1757,14 @@
"@youtubeQualityNote": {
"description": "Note for YouTube service explaining lossy-only quality"
},
+ "youtubeOpusBitrateTitle": "YouTube Opus Bitrate",
+ "@youtubeOpusBitrateTitle": {
+ "description": "Title for YouTube Opus bitrate setting"
+ },
+ "youtubeMp3BitrateTitle": "YouTube MP3 Bitrate",
+ "@youtubeMp3BitrateTitle": {
+ "description": "Title for YouTube MP3 bitrate setting"
+ },
"downloadAskBeforeDownload": "Ask Before Download",
"@downloadAskBeforeDownload": {
"description": "Setting - show quality picker"
@@ -2198,6 +2214,15 @@
"@libraryAboutDescription": {
"description": "Description of local library feature"
},
+ "libraryTracksUnit": "{count, plural, =1{track} other{tracks}}",
+ "@libraryTracksUnit": {
+ "description": "Unit label for tracks count (without the number itself)",
+ "placeholders": {
+ "count": {
+ "type": "int"
+ }
+ }
+ },
"libraryLastScanned": "Last scanned: {time}",
"@libraryLastScanned": {
"description": "Last scan time display",
@@ -2783,6 +2808,283 @@
"@trackConvertFailed": {
"description": "Snackbar when conversion fails"
},
+ "actionCreate": "Create",
+ "@actionCreate": {
+ "description": "Generic action button - create"
+ },
+ "collectionFoldersTitle": "My folders",
+ "@collectionFoldersTitle": {
+ "description": "Library section title for custom folders"
+ },
+ "collectionWishlist": "Wishlist",
+ "@collectionWishlist": {
+ "description": "Custom folder for saved tracks to download later"
+ },
+ "collectionLoved": "Loved",
+ "@collectionLoved": {
+ "description": "Custom folder for favorite tracks"
+ },
+ "collectionPlaylists": "Playlists",
+ "@collectionPlaylists": {
+ "description": "Custom user playlists folder"
+ },
+ "collectionPlaylist": "Playlist",
+ "@collectionPlaylist": {
+ "description": "Single playlist label"
+ },
+ "collectionAddToPlaylist": "Add to playlist",
+ "@collectionAddToPlaylist": {
+ "description": "Action to add a track to user playlist"
+ },
+ "collectionCreatePlaylist": "Create playlist",
+ "@collectionCreatePlaylist": {
+ "description": "Action to create a new playlist"
+ },
+ "collectionNoPlaylistsYet": "No playlists yet",
+ "@collectionNoPlaylistsYet": {
+ "description": "Empty state title when user has no playlists"
+ },
+ "collectionNoPlaylistsSubtitle": "Create a playlist to start categorizing tracks",
+ "@collectionNoPlaylistsSubtitle": {
+ "description": "Empty state subtitle when user has no playlists"
+ },
+ "collectionPlaylistTracks": "{count, plural, =1{1 track} other{{count} tracks}}",
+ "@collectionPlaylistTracks": {
+ "description": "Track count label for custom playlists",
+ "placeholders": {
+ "count": {
+ "type": "int"
+ }
+ }
+ },
+ "collectionAddedToPlaylist": "Added to \"{playlistName}\"",
+ "@collectionAddedToPlaylist": {
+ "description": "Snackbar after adding track to playlist",
+ "placeholders": {
+ "playlistName": {
+ "type": "String"
+ }
+ }
+ },
+ "collectionAlreadyInPlaylist": "Already in \"{playlistName}\"",
+ "@collectionAlreadyInPlaylist": {
+ "description": "Snackbar when track already exists in playlist",
+ "placeholders": {
+ "playlistName": {
+ "type": "String"
+ }
+ }
+ },
+ "collectionPlaylistCreated": "Playlist created",
+ "@collectionPlaylistCreated": {
+ "description": "Snackbar after creating playlist"
+ },
+ "collectionPlaylistNameHint": "Playlist name",
+ "@collectionPlaylistNameHint": {
+ "description": "Hint text for playlist name input"
+ },
+ "collectionPlaylistNameRequired": "Playlist name is required",
+ "@collectionPlaylistNameRequired": {
+ "description": "Validation error for empty playlist name"
+ },
+ "collectionRenamePlaylist": "Rename playlist",
+ "@collectionRenamePlaylist": {
+ "description": "Action to rename playlist"
+ },
+ "collectionDeletePlaylist": "Delete playlist",
+ "@collectionDeletePlaylist": {
+ "description": "Action to delete playlist"
+ },
+ "collectionDeletePlaylistMessage": "Delete \"{playlistName}\" and all tracks inside it?",
+ "@collectionDeletePlaylistMessage": {
+ "description": "Confirmation message for deleting playlist",
+ "placeholders": {
+ "playlistName": {
+ "type": "String"
+ }
+ }
+ },
+ "collectionPlaylistDeleted": "Playlist deleted",
+ "@collectionPlaylistDeleted": {
+ "description": "Snackbar after deleting playlist"
+ },
+ "collectionPlaylistRenamed": "Playlist renamed",
+ "@collectionPlaylistRenamed": {
+ "description": "Snackbar after renaming playlist"
+ },
+ "collectionWishlistEmptyTitle": "Wishlist is empty",
+ "@collectionWishlistEmptyTitle": {
+ "description": "Wishlist empty state title"
+ },
+ "collectionWishlistEmptySubtitle": "Tap + on tracks to save what you want to download later",
+ "@collectionWishlistEmptySubtitle": {
+ "description": "Wishlist empty state subtitle"
+ },
+ "collectionLovedEmptyTitle": "Loved folder is empty",
+ "@collectionLovedEmptyTitle": {
+ "description": "Loved empty state title"
+ },
+ "collectionLovedEmptySubtitle": "Tap love on tracks to keep your favorites",
+ "@collectionLovedEmptySubtitle": {
+ "description": "Loved empty state subtitle"
+ },
+ "collectionPlaylistEmptyTitle": "Playlist is empty",
+ "@collectionPlaylistEmptyTitle": {
+ "description": "Playlist empty state title"
+ },
+ "collectionPlaylistEmptySubtitle": "Long-press + on any track to add it here",
+ "@collectionPlaylistEmptySubtitle": {
+ "description": "Playlist empty state subtitle"
+ },
+ "collectionRemoveFromPlaylist": "Remove from playlist",
+ "@collectionRemoveFromPlaylist": {
+ "description": "Tooltip for removing track from playlist"
+ },
+ "collectionRemoveFromFolder": "Remove from folder",
+ "@collectionRemoveFromFolder": {
+ "description": "Tooltip for removing track from wishlist/loved folder"
+ },
+ "collectionRemoved": "\"{trackName}\" removed",
+ "@collectionRemoved": {
+ "description": "Snackbar after removing a track from a collection",
+ "placeholders": {
+ "trackName": {
+ "type": "String"
+ }
+ }
+ },
+ "collectionAddedToLoved": "\"{trackName}\" added to Loved",
+ "@collectionAddedToLoved": {
+ "description": "Snackbar after adding track to loved folder",
+ "placeholders": {
+ "trackName": {
+ "type": "String"
+ }
+ }
+ },
+ "collectionRemovedFromLoved": "\"{trackName}\" removed from Loved",
+ "@collectionRemovedFromLoved": {
+ "description": "Snackbar after removing track from loved folder",
+ "placeholders": {
+ "trackName": {
+ "type": "String"
+ }
+ }
+ },
+ "collectionAddedToWishlist": "\"{trackName}\" added to Wishlist",
+ "@collectionAddedToWishlist": {
+ "description": "Snackbar after adding track to wishlist",
+ "placeholders": {
+ "trackName": {
+ "type": "String"
+ }
+ }
+ },
+ "collectionRemovedFromWishlist": "\"{trackName}\" removed from Wishlist",
+ "@collectionRemovedFromWishlist": {
+ "description": "Snackbar after removing track from wishlist",
+ "placeholders": {
+ "trackName": {
+ "type": "String"
+ }
+ }
+ },
+ "trackOptionAddToLoved": "Add to Loved",
+ "@trackOptionAddToLoved": {
+ "description": "Bottom sheet action label - add track to loved folder"
+ },
+ "trackOptionRemoveFromLoved": "Remove from Loved",
+ "@trackOptionRemoveFromLoved": {
+ "description": "Bottom sheet action label - remove track from loved folder"
+ },
+ "trackOptionAddToWishlist": "Add to Wishlist",
+ "@trackOptionAddToWishlist": {
+ "description": "Bottom sheet action label - add track to wishlist"
+ },
+ "trackOptionRemoveFromWishlist": "Remove from Wishlist",
+ "@trackOptionRemoveFromWishlist": {
+ "description": "Bottom sheet action label - remove track from wishlist"
+ },
+ "collectionPlaylistChangeCover": "Change cover image",
+ "@collectionPlaylistChangeCover": {
+ "description": "Bottom sheet action to pick a custom cover image for a playlist"
+ },
+ "collectionPlaylistRemoveCover": "Remove cover image",
+ "@collectionPlaylistRemoveCover": {
+ "description": "Bottom sheet action to remove custom cover image from a playlist"
+ },
+ "selectionShareCount": "Share {count} {count, plural, =1{track} other{tracks}}",
+ "@selectionShareCount": {
+ "description": "Share button text with count in selection mode",
+ "placeholders": {
+ "count": {
+ "type": "int"
+ }
+ }
+ },
+ "selectionShareNoFiles": "No shareable files found",
+ "@selectionShareNoFiles": {
+ "description": "Snackbar when no selected files exist on disk"
+ },
+ "selectionConvertCount": "Convert {count} {count, plural, =1{track} other{tracks}}",
+ "@selectionConvertCount": {
+ "description": "Convert button text with count in selection mode",
+ "placeholders": {
+ "count": {
+ "type": "int"
+ }
+ }
+ },
+ "selectionConvertNoConvertible": "No convertible tracks selected",
+ "@selectionConvertNoConvertible": {
+ "description": "Snackbar when no selected tracks support conversion"
+ },
+ "selectionBatchConvertConfirmTitle": "Batch Convert",
+ "@selectionBatchConvertConfirmTitle": {
+ "description": "Confirmation dialog title for batch conversion"
+ },
+ "selectionBatchConvertConfirmMessage": "Convert {count} {count, plural, =1{track} other{tracks}} to {format} at {bitrate}?\n\nOriginal files will be deleted after conversion.",
+ "@selectionBatchConvertConfirmMessage": {
+ "description": "Confirmation dialog message for batch conversion",
+ "placeholders": {
+ "count": {
+ "type": "int"
+ },
+ "format": {
+ "type": "String"
+ },
+ "bitrate": {
+ "type": "String"
+ }
+ }
+ },
+ "selectionBatchConvertProgress": "Converting {current} of {total}...",
+ "@selectionBatchConvertProgress": {
+ "description": "Snackbar during batch conversion progress",
+ "placeholders": {
+ "current": {
+ "type": "int"
+ },
+ "total": {
+ "type": "int"
+ }
+ }
+ },
+ "selectionBatchConvertSuccess": "Converted {success} of {total} tracks to {format}",
+ "@selectionBatchConvertSuccess": {
+ "description": "Snackbar after batch conversion completes",
+ "placeholders": {
+ "success": {
+ "type": "int"
+ },
+ "total": {
+ "type": "int"
+ },
+ "format": {
+ "type": "String"
+ }
+ }
+ },
"downloadedAlbumDownloadedCount": "{count} downloaded",
"@downloadedAlbumDownloadedCount": {
"description": "Downloaded tracks count badge",
@@ -2800,4 +3102,4 @@
"@downloadUseAlbumArtistForFoldersTrackSubtitle": {
"description": "Subtitle when Track Artist is used for folder naming"
}
-}
+}
\ No newline at end of file
diff --git a/lib/l10n/arb/app_nl.arb b/lib/l10n/arb/app_nl.arb
index 1fd0bec6..f2b8bb02 100644
--- a/lib/l10n/arb/app_nl.arb
+++ b/lib/l10n/arb/app_nl.arb
@@ -991,6 +991,14 @@
"@filenameFormat": {
"description": "Setting title - filename pattern"
},
+ "filenameShowAdvancedTags": "Show advanced tags",
+ "@filenameShowAdvancedTags": {
+ "description": "Toggle label for showing advanced filename tags"
+ },
+ "filenameShowAdvancedTagsDescription": "Enable formatted tags for track padding and date patterns",
+ "@filenameShowAdvancedTagsDescription": {
+ "description": "Description for advanced filename tag toggle"
+ },
"folderOrganizationNone": "No organization",
"@folderOrganizationNone": {
"description": "Folder option - flat structure"
@@ -1749,6 +1757,14 @@
"@youtubeQualityNote": {
"description": "Note for YouTube service explaining lossy-only quality"
},
+ "youtubeOpusBitrateTitle": "YouTube Opus Bitrate",
+ "@youtubeOpusBitrateTitle": {
+ "description": "Title for YouTube Opus bitrate setting"
+ },
+ "youtubeMp3BitrateTitle": "YouTube MP3 Bitrate",
+ "@youtubeMp3BitrateTitle": {
+ "description": "Title for YouTube MP3 bitrate setting"
+ },
"downloadAskBeforeDownload": "Ask Before Download",
"@downloadAskBeforeDownload": {
"description": "Setting - show quality picker"
@@ -2198,6 +2214,15 @@
"@libraryAboutDescription": {
"description": "Description of local library feature"
},
+ "libraryTracksUnit": "{count, plural, =1{track} other{tracks}}",
+ "@libraryTracksUnit": {
+ "description": "Unit label for tracks count (without the number itself)",
+ "placeholders": {
+ "count": {
+ "type": "int"
+ }
+ }
+ },
"libraryLastScanned": "Last scanned: {time}",
"@libraryLastScanned": {
"description": "Last scan time display",
@@ -2783,6 +2808,283 @@
"@trackConvertFailed": {
"description": "Snackbar when conversion fails"
},
+ "actionCreate": "Create",
+ "@actionCreate": {
+ "description": "Generic action button - create"
+ },
+ "collectionFoldersTitle": "My folders",
+ "@collectionFoldersTitle": {
+ "description": "Library section title for custom folders"
+ },
+ "collectionWishlist": "Wishlist",
+ "@collectionWishlist": {
+ "description": "Custom folder for saved tracks to download later"
+ },
+ "collectionLoved": "Loved",
+ "@collectionLoved": {
+ "description": "Custom folder for favorite tracks"
+ },
+ "collectionPlaylists": "Playlists",
+ "@collectionPlaylists": {
+ "description": "Custom user playlists folder"
+ },
+ "collectionPlaylist": "Playlist",
+ "@collectionPlaylist": {
+ "description": "Single playlist label"
+ },
+ "collectionAddToPlaylist": "Add to playlist",
+ "@collectionAddToPlaylist": {
+ "description": "Action to add a track to user playlist"
+ },
+ "collectionCreatePlaylist": "Create playlist",
+ "@collectionCreatePlaylist": {
+ "description": "Action to create a new playlist"
+ },
+ "collectionNoPlaylistsYet": "No playlists yet",
+ "@collectionNoPlaylistsYet": {
+ "description": "Empty state title when user has no playlists"
+ },
+ "collectionNoPlaylistsSubtitle": "Create a playlist to start categorizing tracks",
+ "@collectionNoPlaylistsSubtitle": {
+ "description": "Empty state subtitle when user has no playlists"
+ },
+ "collectionPlaylistTracks": "{count, plural, =1{1 track} other{{count} tracks}}",
+ "@collectionPlaylistTracks": {
+ "description": "Track count label for custom playlists",
+ "placeholders": {
+ "count": {
+ "type": "int"
+ }
+ }
+ },
+ "collectionAddedToPlaylist": "Added to \"{playlistName}\"",
+ "@collectionAddedToPlaylist": {
+ "description": "Snackbar after adding track to playlist",
+ "placeholders": {
+ "playlistName": {
+ "type": "String"
+ }
+ }
+ },
+ "collectionAlreadyInPlaylist": "Already in \"{playlistName}\"",
+ "@collectionAlreadyInPlaylist": {
+ "description": "Snackbar when track already exists in playlist",
+ "placeholders": {
+ "playlistName": {
+ "type": "String"
+ }
+ }
+ },
+ "collectionPlaylistCreated": "Playlist created",
+ "@collectionPlaylistCreated": {
+ "description": "Snackbar after creating playlist"
+ },
+ "collectionPlaylistNameHint": "Playlist name",
+ "@collectionPlaylistNameHint": {
+ "description": "Hint text for playlist name input"
+ },
+ "collectionPlaylistNameRequired": "Playlist name is required",
+ "@collectionPlaylistNameRequired": {
+ "description": "Validation error for empty playlist name"
+ },
+ "collectionRenamePlaylist": "Rename playlist",
+ "@collectionRenamePlaylist": {
+ "description": "Action to rename playlist"
+ },
+ "collectionDeletePlaylist": "Delete playlist",
+ "@collectionDeletePlaylist": {
+ "description": "Action to delete playlist"
+ },
+ "collectionDeletePlaylistMessage": "Delete \"{playlistName}\" and all tracks inside it?",
+ "@collectionDeletePlaylistMessage": {
+ "description": "Confirmation message for deleting playlist",
+ "placeholders": {
+ "playlistName": {
+ "type": "String"
+ }
+ }
+ },
+ "collectionPlaylistDeleted": "Playlist deleted",
+ "@collectionPlaylistDeleted": {
+ "description": "Snackbar after deleting playlist"
+ },
+ "collectionPlaylistRenamed": "Playlist renamed",
+ "@collectionPlaylistRenamed": {
+ "description": "Snackbar after renaming playlist"
+ },
+ "collectionWishlistEmptyTitle": "Wishlist is empty",
+ "@collectionWishlistEmptyTitle": {
+ "description": "Wishlist empty state title"
+ },
+ "collectionWishlistEmptySubtitle": "Tap + on tracks to save what you want to download later",
+ "@collectionWishlistEmptySubtitle": {
+ "description": "Wishlist empty state subtitle"
+ },
+ "collectionLovedEmptyTitle": "Loved folder is empty",
+ "@collectionLovedEmptyTitle": {
+ "description": "Loved empty state title"
+ },
+ "collectionLovedEmptySubtitle": "Tap love on tracks to keep your favorites",
+ "@collectionLovedEmptySubtitle": {
+ "description": "Loved empty state subtitle"
+ },
+ "collectionPlaylistEmptyTitle": "Playlist is empty",
+ "@collectionPlaylistEmptyTitle": {
+ "description": "Playlist empty state title"
+ },
+ "collectionPlaylistEmptySubtitle": "Long-press + on any track to add it here",
+ "@collectionPlaylistEmptySubtitle": {
+ "description": "Playlist empty state subtitle"
+ },
+ "collectionRemoveFromPlaylist": "Remove from playlist",
+ "@collectionRemoveFromPlaylist": {
+ "description": "Tooltip for removing track from playlist"
+ },
+ "collectionRemoveFromFolder": "Remove from folder",
+ "@collectionRemoveFromFolder": {
+ "description": "Tooltip for removing track from wishlist/loved folder"
+ },
+ "collectionRemoved": "\"{trackName}\" removed",
+ "@collectionRemoved": {
+ "description": "Snackbar after removing a track from a collection",
+ "placeholders": {
+ "trackName": {
+ "type": "String"
+ }
+ }
+ },
+ "collectionAddedToLoved": "\"{trackName}\" added to Loved",
+ "@collectionAddedToLoved": {
+ "description": "Snackbar after adding track to loved folder",
+ "placeholders": {
+ "trackName": {
+ "type": "String"
+ }
+ }
+ },
+ "collectionRemovedFromLoved": "\"{trackName}\" removed from Loved",
+ "@collectionRemovedFromLoved": {
+ "description": "Snackbar after removing track from loved folder",
+ "placeholders": {
+ "trackName": {
+ "type": "String"
+ }
+ }
+ },
+ "collectionAddedToWishlist": "\"{trackName}\" added to Wishlist",
+ "@collectionAddedToWishlist": {
+ "description": "Snackbar after adding track to wishlist",
+ "placeholders": {
+ "trackName": {
+ "type": "String"
+ }
+ }
+ },
+ "collectionRemovedFromWishlist": "\"{trackName}\" removed from Wishlist",
+ "@collectionRemovedFromWishlist": {
+ "description": "Snackbar after removing track from wishlist",
+ "placeholders": {
+ "trackName": {
+ "type": "String"
+ }
+ }
+ },
+ "trackOptionAddToLoved": "Add to Loved",
+ "@trackOptionAddToLoved": {
+ "description": "Bottom sheet action label - add track to loved folder"
+ },
+ "trackOptionRemoveFromLoved": "Remove from Loved",
+ "@trackOptionRemoveFromLoved": {
+ "description": "Bottom sheet action label - remove track from loved folder"
+ },
+ "trackOptionAddToWishlist": "Add to Wishlist",
+ "@trackOptionAddToWishlist": {
+ "description": "Bottom sheet action label - add track to wishlist"
+ },
+ "trackOptionRemoveFromWishlist": "Remove from Wishlist",
+ "@trackOptionRemoveFromWishlist": {
+ "description": "Bottom sheet action label - remove track from wishlist"
+ },
+ "collectionPlaylistChangeCover": "Change cover image",
+ "@collectionPlaylistChangeCover": {
+ "description": "Bottom sheet action to pick a custom cover image for a playlist"
+ },
+ "collectionPlaylistRemoveCover": "Remove cover image",
+ "@collectionPlaylistRemoveCover": {
+ "description": "Bottom sheet action to remove custom cover image from a playlist"
+ },
+ "selectionShareCount": "Share {count} {count, plural, =1{track} other{tracks}}",
+ "@selectionShareCount": {
+ "description": "Share button text with count in selection mode",
+ "placeholders": {
+ "count": {
+ "type": "int"
+ }
+ }
+ },
+ "selectionShareNoFiles": "No shareable files found",
+ "@selectionShareNoFiles": {
+ "description": "Snackbar when no selected files exist on disk"
+ },
+ "selectionConvertCount": "Convert {count} {count, plural, =1{track} other{tracks}}",
+ "@selectionConvertCount": {
+ "description": "Convert button text with count in selection mode",
+ "placeholders": {
+ "count": {
+ "type": "int"
+ }
+ }
+ },
+ "selectionConvertNoConvertible": "No convertible tracks selected",
+ "@selectionConvertNoConvertible": {
+ "description": "Snackbar when no selected tracks support conversion"
+ },
+ "selectionBatchConvertConfirmTitle": "Batch Convert",
+ "@selectionBatchConvertConfirmTitle": {
+ "description": "Confirmation dialog title for batch conversion"
+ },
+ "selectionBatchConvertConfirmMessage": "Convert {count} {count, plural, =1{track} other{tracks}} to {format} at {bitrate}?\n\nOriginal files will be deleted after conversion.",
+ "@selectionBatchConvertConfirmMessage": {
+ "description": "Confirmation dialog message for batch conversion",
+ "placeholders": {
+ "count": {
+ "type": "int"
+ },
+ "format": {
+ "type": "String"
+ },
+ "bitrate": {
+ "type": "String"
+ }
+ }
+ },
+ "selectionBatchConvertProgress": "Converting {current} of {total}...",
+ "@selectionBatchConvertProgress": {
+ "description": "Snackbar during batch conversion progress",
+ "placeholders": {
+ "current": {
+ "type": "int"
+ },
+ "total": {
+ "type": "int"
+ }
+ }
+ },
+ "selectionBatchConvertSuccess": "Converted {success} of {total} tracks to {format}",
+ "@selectionBatchConvertSuccess": {
+ "description": "Snackbar after batch conversion completes",
+ "placeholders": {
+ "success": {
+ "type": "int"
+ },
+ "total": {
+ "type": "int"
+ },
+ "format": {
+ "type": "String"
+ }
+ }
+ },
"downloadedAlbumDownloadedCount": "{count} downloaded",
"@downloadedAlbumDownloadedCount": {
"description": "Downloaded tracks count badge",
@@ -2800,4 +3102,4 @@
"@downloadUseAlbumArtistForFoldersTrackSubtitle": {
"description": "Subtitle when Track Artist is used for folder naming"
}
-}
+}
\ No newline at end of file
diff --git a/lib/l10n/arb/app_pt.arb b/lib/l10n/arb/app_pt.arb
index 503247dd..27a0b5e2 100644
--- a/lib/l10n/arb/app_pt.arb
+++ b/lib/l10n/arb/app_pt.arb
@@ -402,7 +402,7 @@
"@aboutDabMusicDesc": {
"description": "Credit for DAB Music API"
},
- "aboutAppDescription": "Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.",
+ "aboutAppDescription": "Download Spotify tracks in lossless quality from Tidal and Qobuz.",
"@aboutAppDescription": {
"description": "App description in header card"
},
@@ -1005,7 +1005,7 @@
},
"providerBuiltIn": "Built-in",
"@providerBuiltIn": {
- "description": "Label for built-in providers (Tidal/Qobuz/Amazon)"
+ "description": "Label for built-in providers (Tidal/Qobuz)"
},
"providerExtension": "Extension",
"@providerExtension": {
diff --git a/lib/l10n/arb/app_pt_PT.arb b/lib/l10n/arb/app_pt_PT.arb
index 8d844396..73190e72 100644
--- a/lib/l10n/arb/app_pt_PT.arb
+++ b/lib/l10n/arb/app_pt_PT.arb
@@ -450,7 +450,7 @@
"@aboutSpotiSaverDesc": {
"description": "Credit for SpotiSaver API"
},
- "aboutAppDescription": "Baixe faixas do Spotify em qualidade sem perdas do Tidal, Qobuz e Amazon Music.",
+ "aboutAppDescription": "Baixe faixas do Spotify em qualidade sem perdas do Tidal e Qobuz.",
"@aboutAppDescription": {
"description": "App description in header card"
},
@@ -1089,7 +1089,7 @@
},
"providerBuiltIn": "Embutido",
"@providerBuiltIn": {
- "description": "Label for built-in providers (Tidal/Qobuz/Amazon)"
+ "description": "Label for built-in providers (Tidal/Qobuz)"
},
"providerExtension": "Extensão",
"@providerExtension": {
@@ -2358,7 +2358,7 @@
"@tutorialWelcomeTip1": {
"description": "Tutorial welcome tip 1"
},
- "tutorialWelcomeTip2": "Get FLAC quality audio from Tidal, Qobuz, or Amazon Music",
+ "tutorialWelcomeTip2": "Obtenha áudio em qualidade FLAC do Tidal, Qobuz ou Deezer",
"@tutorialWelcomeTip2": {
"description": "Tutorial welcome tip 2"
},
diff --git a/lib/l10n/arb/app_ru.arb b/lib/l10n/arb/app_ru.arb
index 4ea4e618..10f1fce3 100644
--- a/lib/l10n/arb/app_ru.arb
+++ b/lib/l10n/arb/app_ru.arb
@@ -77,7 +77,7 @@
"@settingsAbout": {
"description": "Settings section - app info"
},
- "downloadTitle": "Скачивание",
+ "downloadTitle": "Скачать",
"@downloadTitle": {
"description": "Download settings page title"
},
@@ -174,11 +174,11 @@
"@optionsUseExtensionProvidersOff": {
"description": "Status when extension providers disabled"
},
- "optionsEmbedLyrics": "Вставить текст песни",
+ "optionsEmbedLyrics": "Вписать текст песни",
"@optionsEmbedLyrics": {
"description": "Embed lyrics in audio files"
},
- "optionsEmbedLyricsSubtitle": "Вставить синхронизированные тексты в FLAC файлы",
+ "optionsEmbedLyricsSubtitle": "Вписать синхронизированные тексты во FLAC файлы",
"@optionsEmbedLyricsSubtitle": {
"description": "Subtitle for embed lyrics"
},
@@ -422,7 +422,7 @@
"@aboutVersion": {
"description": "Version info label"
},
- "aboutBinimumDesc": "Создатель QQDL & HiFi API. Без этого API загрузки Tidal не существовали бы!",
+ "aboutBinimumDesc": "Создатель QQDL & HiFi API. Без него API загрузки Tidal не существовали бы!",
"@aboutBinimumDesc": {
"description": "Credit description for binimum"
},
@@ -728,7 +728,7 @@
"@dialogDeleteSelectedTitle": {
"description": "Dialog title - delete selected items"
},
- "dialogDeleteSelectedMessage": "Удалить {count} {count, plural, one {трек} few {трека} many {треков} other {треков}} из истории?\n\nЭто также удалит файлы из хранилища.",
+ "dialogDeleteSelectedMessage": "Удалить {count} {count, plural, one {трек} few {трека} many {треков} other{треков}} из истории?\n\nЭто также удалит файлы из хранилища.",
"@dialogDeleteSelectedMessage": {
"description": "Dialog message - delete selected tracks",
"placeholders": {
@@ -742,7 +742,7 @@
"description": "Dialog title - import CSV playlist"
},
"dialogImportPlaylistMessage": "Найдено {count} треков в CSV. Добавить их в очередь загрузки?",
- "csvImportTracks": "{count} треков из CSV",
+ "csvImportTracks": "{count} трек(-ов) из CSV",
"@csvImportTracks": {
"description": "Label shown in quality picker for CSV import",
"placeholders": {
@@ -807,7 +807,7 @@
"@snackbarCredentialsCleared": {
"description": "Snackbar - Spotify credentials removed"
},
- "snackbarDeletedTracks": "Удалено {count} {count, plural, one {трек} few {трека} many {треков} other {треков}}",
+ "snackbarDeletedTracks": "Удалено {count} {count, plural, one {трек} few {трека} many {треков} other{треков}}",
"@snackbarDeletedTracks": {
"description": "Snackbar - tracks deleted",
"placeholders": {
@@ -991,6 +991,14 @@
"@filenameFormat": {
"description": "Setting title - filename pattern"
},
+ "filenameShowAdvancedTags": "Показать расширенные теги",
+ "@filenameShowAdvancedTags": {
+ "description": "Toggle label for showing advanced filename tags"
+ },
+ "filenameShowAdvancedTagsDescription": "Включить форматированные теги для отслеживания заполнения и шаблонов дат",
+ "@filenameShowAdvancedTagsDescription": {
+ "description": "Description for advanced filename tag toggle"
+ },
"folderOrganizationNone": "Без организации",
"@folderOrganizationNone": {
"description": "Folder option - flat structure"
@@ -1261,7 +1269,7 @@
"@lyricsModeDescription": {
"description": "Lyrics mode picker description"
},
- "lyricsModeEmbed": "Вставить в файл",
+ "lyricsModeEmbed": "Вписать в файл",
"@lyricsModeEmbed": {
"description": "Lyrics mode option - embed in audio file"
},
@@ -1281,7 +1289,7 @@
"@lyricsModeBoth": {
"description": "Lyrics mode option - embed and external"
},
- "lyricsModeBothSubtitle": "Вставить и сохранить файл .lrc",
+ "lyricsModeBothSubtitle": "Вписать и сохранить .lrc файл",
"@lyricsModeBothSubtitle": {
"description": "Subtitle for both option"
},
@@ -1455,7 +1463,7 @@
"@trackLyricsLoadFailed": {
"description": "Message when lyrics loading fails"
},
- "trackEmbedLyrics": "Вставить текст песни",
+ "trackEmbedLyrics": "Вписать текст песни",
"@trackEmbedLyrics": {
"description": "Action - embed lyrics into audio file"
},
@@ -1749,6 +1757,14 @@
"@youtubeQualityNote": {
"description": "Note for YouTube service explaining lossy-only quality"
},
+ "youtubeOpusBitrateTitle": "Битрейт YouTube Opus",
+ "@youtubeOpusBitrateTitle": {
+ "description": "Title for YouTube Opus bitrate setting"
+ },
+ "youtubeMp3BitrateTitle": "Битрейт YouTube MP3",
+ "@youtubeMp3BitrateTitle": {
+ "description": "Title for YouTube MP3 bitrate setting"
+ },
"downloadAskBeforeDownload": "Спрашивать перед скачиванием",
"@downloadAskBeforeDownload": {
"description": "Setting - show quality picker"
@@ -1769,7 +1785,7 @@
"@downloadUseAlbumArtistForFolders": {
"description": "Setting - choose whether artist folders use Album Artist or Track Artist"
},
- "downloadUsePrimaryArtistOnly": "Primary artist only for folders",
+ "downloadUsePrimaryArtistOnly": "Основной исполнитель только для папок",
"@downloadUsePrimaryArtistOnly": {
"description": "Setting - strip featured artists from folder name"
},
@@ -1777,7 +1793,7 @@
"@downloadUsePrimaryArtistOnlyEnabled": {
"description": "Subtitle when primary artist only is enabled"
},
- "downloadUsePrimaryArtistOnlyDisabled": "Full artist string used for folder name",
+ "downloadUsePrimaryArtistOnlyDisabled": "Полная строка исполнителя, используемая для имени папки",
"@downloadUsePrimaryArtistOnlyDisabled": {
"description": "Subtitle when primary artist only is disabled"
},
@@ -1817,7 +1833,7 @@
"@settingsDownloadNetwork": {
"description": "Setting for network type preference"
},
- "settingsDownloadNetworkAny": "WiFi и мобильная сеть",
+ "settingsDownloadNetworkAny": "WiFi и Мобильная сеть",
"@settingsDownloadNetworkAny": {
"description": "Network option - use any connection"
},
@@ -1873,7 +1889,7 @@
"@downloadedAlbumDeleteSelected": {
"description": "Button - delete selected tracks"
},
- "downloadedAlbumDeleteMessage": "Удалить {count} {count, plural, one {трек} few {трека} many {треков} other {треков}} из этого альбома?\n\nЭто также удалит файлы из хранилища.",
+ "downloadedAlbumDeleteMessage": "Удалить {count} {count, plural, one {трек} few {трека} many {треков} other{треков}} из этого альбома?\n\nЭто также удалит файлы из хранилища.",
"@downloadedAlbumDeleteMessage": {
"description": "Delete confirmation with count",
"placeholders": {
@@ -1899,7 +1915,7 @@
"@downloadedAlbumTapToSelect": {
"description": "Selection hint"
},
- "downloadedAlbumDeleteCount": "Удалить {count} {count, plural, one {трек} few {трека} many {треков} other {треков}}",
+ "downloadedAlbumDeleteCount": "Удалить {count} {count, plural, one {трек} few {трека} many {треков} other{треков}}",
"@downloadedAlbumDeleteCount": {
"description": "Delete button text with count",
"placeholders": {
@@ -2198,6 +2214,15 @@
"@libraryAboutDescription": {
"description": "Description of local library feature"
},
+ "libraryTracksUnit": "{count, plural, one {трек} few {трека} many {треков} other{треков}}",
+ "@libraryTracksUnit": {
+ "description": "Unit label for tracks count (without the number itself)",
+ "placeholders": {
+ "count": {
+ "type": "int"
+ }
+ }
+ },
"libraryLastScanned": "Последнее сканирование: {time}",
"@libraryLastScanned": {
"description": "Last scan time display",
@@ -2304,7 +2329,7 @@
"@libraryFilterQualityCD": {
"description": "Filter option - CD quality audio"
},
- "libraryFilterQualityLossy": "С потерями",
+ "libraryFilterQualityLossy": "Lossy",
"@libraryFilterQualityLossy": {
"description": "Filter option - lossy compressed audio"
},
@@ -2410,7 +2435,7 @@
"@tutorialExtensionsDesc": {
"description": "Tutorial extensions page description"
},
- "tutorialExtensionsTip1": "Browse the Store tab to discover useful extensions",
+ "tutorialExtensionsTip1": "Просмотрите вкладку Магазина, чтобы найти полезные расширения",
"@tutorialExtensionsTip1": {
"description": "Tutorial extensions tip 1"
},
@@ -2418,7 +2443,7 @@
"@tutorialExtensionsTip2": {
"description": "Tutorial extensions tip 2"
},
- "tutorialExtensionsTip3": "Get lyrics, enhanced metadata, and more features",
+ "tutorialExtensionsTip3": "Получайте тексты песен, улучшенные метаданные и другие возможности",
"@tutorialExtensionsTip3": {
"description": "Tutorial extensions tip 3"
},
@@ -2426,7 +2451,7 @@
"@tutorialSettingsTitle": {
"description": "Tutorial settings page title"
},
- "tutorialSettingsDesc": "Personalize the app in Settings to match your preferences.",
+ "tutorialSettingsDesc": "Персонализируйте приложение в Настройках, чтобы оно соответствовало вашим предпочтениям.",
"@tutorialSettingsDesc": {
"description": "Tutorial settings page description"
},
@@ -2454,11 +2479,11 @@
"@libraryForceFullScanSubtitle": {
"description": "Subtitle for force full scan button"
},
- "cleanupOrphanedDownloads": "Cleanup Orphaned Downloads",
+ "cleanupOrphanedDownloads": "Очистка отложенных скачиваний",
"@cleanupOrphanedDownloads": {
"description": "Button to remove history entries for deleted files"
},
- "cleanupOrphanedDownloadsSubtitle": "Remove history entries for files that no longer exist",
+ "cleanupOrphanedDownloadsSubtitle": "Удалить историю записи для файлов, которых больше не существует",
"@cleanupOrphanedDownloadsSubtitle": {
"description": "Subtitle for orphaned cleanup button"
},
@@ -2471,7 +2496,7 @@
}
}
},
- "cleanupOrphanedDownloadsNone": "No orphaned entries found",
+ "cleanupOrphanedDownloadsNone": "Записей без описания не найдено",
"@cleanupOrphanedDownloadsNone": {
"description": "Snackbar when no orphans found"
},
@@ -2483,11 +2508,11 @@
"@cacheSummaryTitle": {
"description": "Heading for cache summary card"
},
- "cacheSummarySubtitle": "Clearing cache will not remove downloaded music files.",
+ "cacheSummarySubtitle": "Очистка кэша не приведет к удалению загруженных музыкальных файлов.",
"@cacheSummarySubtitle": {
"description": "Helper text for cache summary card"
},
- "cacheEstimatedTotal": "Estimated cache usage: {size}",
+ "cacheEstimatedTotal": "Приблизительное использование кэша: {size}",
"@cacheEstimatedTotal": {
"description": "Total cache size shown in summary",
"placeholders": {
@@ -2508,47 +2533,47 @@
"@cacheAppDirectory": {
"description": "Cache item title for app cache directory"
},
- "cacheAppDirectoryDesc": "HTTP responses, WebView data, and other temporary app data.",
+ "cacheAppDirectoryDesc": "HTTP-ответы, данные WebView и другие временные данные приложения.",
"@cacheAppDirectoryDesc": {
"description": "Description of what app cache directory contains"
},
- "cacheTempDirectory": "Temporary directory",
+ "cacheTempDirectory": "Временная директория",
"@cacheTempDirectory": {
"description": "Cache item title for temporary files directory"
},
- "cacheTempDirectoryDesc": "Temporary files from downloads and audio conversion.",
+ "cacheTempDirectoryDesc": "Временные файлы из загрузок и аудио конвертации.",
"@cacheTempDirectoryDesc": {
"description": "Description of what temporary directory contains"
},
- "cacheCoverImage": "Cover image cache",
+ "cacheCoverImage": "Кэш обложек",
"@cacheCoverImage": {
"description": "Cache item title for persistent cover images"
},
- "cacheCoverImageDesc": "Downloaded album and track cover art. Will re-download when viewed.",
+ "cacheCoverImageDesc": "Скачанный альбом и трек обложки. Будет заново скачан после просмотра.",
"@cacheCoverImageDesc": {
"description": "Description of what cover image cache contains"
},
- "cacheLibraryCover": "Library cover cache",
+ "cacheLibraryCover": "Кэш обложек библиотеки",
"@cacheLibraryCover": {
"description": "Cache item title for local library cover art images"
},
- "cacheLibraryCoverDesc": "Cover art extracted from local music files. Will re-extract on next scan.",
+ "cacheLibraryCoverDesc": "Обложка извлечена из локальных музыкальных файлов. Будет повторно извлечено при следующем сканировании.",
"@cacheLibraryCoverDesc": {
"description": "Description of what library cover cache contains"
},
- "cacheExploreFeed": "Explore feed cache",
+ "cacheExploreFeed": "Просмотреть кэш ленты",
"@cacheExploreFeed": {
"description": "Cache item title for explore home feed cache"
},
- "cacheExploreFeedDesc": "Explore tab content (new releases, trending). Will refresh on next visit.",
+ "cacheExploreFeedDesc": "Изучите содержимое вкладки (новые релизы, тренды). Они обновятся при следующем посещении.",
"@cacheExploreFeedDesc": {
"description": "Description of what explore feed cache contains"
},
- "cacheTrackLookup": "Track lookup cache",
+ "cacheTrackLookup": "Отслеживать кэш поиска",
"@cacheTrackLookup": {
"description": "Cache item title for track ID lookup cache"
},
- "cacheTrackLookupDesc": "Spotify/Deezer track ID lookups. Clearing may slow next few searches.",
+ "cacheTrackLookupDesc": "Поиск ID трека в Spotify/Deezer. Очистка может замедлить следующие несколько поисков.",
"@cacheTrackLookupDesc": {
"description": "Description of what track lookup cache contains"
},
@@ -2581,7 +2606,7 @@
}
}
},
- "cacheEntries": "{count} entries",
+ "cacheEntries": "{count} записей",
"@cacheEntries": {
"description": "Track cache entry count",
"placeholders": {
@@ -2603,7 +2628,7 @@
"@cacheClearConfirmTitle": {
"description": "Dialog title before clearing one cache category"
},
- "cacheClearConfirmMessage": "This will clear cached data for {target}. Downloaded music files will not be deleted.",
+ "cacheClearConfirmMessage": "Это очистит кэш для {target}. Загруженные музыкальные файлы не будут удалены.",
"@cacheClearConfirmMessage": {
"description": "Dialog message before clearing selected cache",
"placeholders": {
@@ -2632,7 +2657,7 @@
"@cacheCleanupUnusedSubtitle": {
"description": "Subtitle for cleanup unused data action"
},
- "cacheCleanupResult": "Cleanup completed: {downloadCount} orphaned downloads, {libraryCount} missing library entries",
+ "cacheCleanupResult": "Очистка завершена: {downloadCount} потерянных загрузок, {libraryCount} отсутствующих записей в библиотеке",
"@cacheCleanupResult": {
"description": "Snackbar after unused data cleanup",
"placeholders": {
@@ -2664,15 +2689,15 @@
"@trackSaveLyricsSubtitle": {
"description": "Subtitle for save lyrics action"
},
- "trackSaveLyricsProgress": "Saving lyrics...",
+ "trackSaveLyricsProgress": "Сохранение текста...",
"@trackSaveLyricsProgress": {
"description": "Snackbar while saving lyrics to file"
},
- "trackReEnrich": "Re-enrich",
+ "trackReEnrich": "Обновить",
"@trackReEnrich": {
"description": "Menu action - re-embed metadata into audio file"
},
- "trackReEnrichOnlineSubtitle": "Search metadata online and embed into file",
+ "trackReEnrichOnlineSubtitle": "Поиск в сети метаданных и встраивание в файл",
"@trackReEnrichOnlineSubtitle": {
"description": "Subtitle for re-enrich metadata action for local items"
},
@@ -2702,7 +2727,7 @@
}
}
},
- "trackReEnrichProgress": "Re-enriching metadata...",
+ "trackReEnrichProgress": "Обновление метаданных...",
"@trackReEnrichProgress": {
"description": "Snackbar while re-enriching metadata"
},
@@ -2710,7 +2735,7 @@
"@trackReEnrichSearching": {
"description": "Snackbar while searching metadata from internet for local items"
},
- "trackReEnrichSuccess": "Metadata re-enriched successfully",
+ "trackReEnrichSuccess": "Метаданные успешно обновлены",
"@trackReEnrichSuccess": {
"description": "Snackbar after successful re-enrichment"
},
@@ -2727,31 +2752,31 @@
}
}
},
- "trackConvertFormat": "Convert Format",
+ "trackConvertFormat": "Переконвертировать формат",
"@trackConvertFormat": {
"description": "Menu item - convert audio format"
},
- "trackConvertFormatSubtitle": "Convert to MP3 or Opus",
+ "trackConvertFormatSubtitle": "Конвертировать в MP3 или Opus",
"@trackConvertFormatSubtitle": {
"description": "Subtitle for convert format menu item"
},
- "trackConvertTitle": "Convert Audio",
+ "trackConvertTitle": "Конвертировать аудио",
"@trackConvertTitle": {
"description": "Title of convert bottom sheet"
},
- "trackConvertTargetFormat": "Target Format",
+ "trackConvertTargetFormat": "Целевой формат",
"@trackConvertTargetFormat": {
"description": "Label for format selection"
},
- "trackConvertBitrate": "Bitrate",
+ "trackConvertBitrate": "Битрейт",
"@trackConvertBitrate": {
"description": "Label for bitrate selection"
},
- "trackConvertConfirmTitle": "Confirm Conversion",
+ "trackConvertConfirmTitle": "Подтвердить конвертацию",
"@trackConvertConfirmTitle": {
"description": "Confirmation dialog title"
},
- "trackConvertConfirmMessage": "Convert from {sourceFormat} to {targetFormat} at {bitrate}?\n\nThe original file will be deleted after conversion.",
+ "trackConvertConfirmMessage": "Конвертировать из {sourceFormat} в {targetFormat} {bitrate}?\n\nОригинальный файл будет удален после конвертации.",
"@trackConvertConfirmMessage": {
"description": "Confirmation dialog message",
"placeholders": {
@@ -2766,11 +2791,11 @@
}
}
},
- "trackConvertConverting": "Converting audio...",
+ "trackConvertConverting": "Конвертация аудио...",
"@trackConvertConverting": {
"description": "Snackbar while converting"
},
- "trackConvertSuccess": "Converted to {format} successfully",
+ "trackConvertSuccess": "Успешно конвертировано в {format}",
"@trackConvertSuccess": {
"description": "Snackbar after successful conversion",
"placeholders": {
@@ -2779,10 +2804,287 @@
}
}
},
- "trackConvertFailed": "Conversion failed",
+ "trackConvertFailed": "Ошибка конвертации",
"@trackConvertFailed": {
"description": "Snackbar when conversion fails"
},
+ "actionCreate": "Создать",
+ "@actionCreate": {
+ "description": "Generic action button - create"
+ },
+ "collectionFoldersTitle": "Мои папки",
+ "@collectionFoldersTitle": {
+ "description": "Library section title for custom folders"
+ },
+ "collectionWishlist": "Список желаемого",
+ "@collectionWishlist": {
+ "description": "Custom folder for saved tracks to download later"
+ },
+ "collectionLoved": "Любимые",
+ "@collectionLoved": {
+ "description": "Custom folder for favorite tracks"
+ },
+ "collectionPlaylists": "Плейлисты",
+ "@collectionPlaylists": {
+ "description": "Custom user playlists folder"
+ },
+ "collectionPlaylist": "Плейлист",
+ "@collectionPlaylist": {
+ "description": "Single playlist label"
+ },
+ "collectionAddToPlaylist": "Добавить в плейлист",
+ "@collectionAddToPlaylist": {
+ "description": "Action to add a track to user playlist"
+ },
+ "collectionCreatePlaylist": "Создать плейлист",
+ "@collectionCreatePlaylist": {
+ "description": "Action to create a new playlist"
+ },
+ "collectionNoPlaylistsYet": "Плейлисты отсутствуют",
+ "@collectionNoPlaylistsYet": {
+ "description": "Empty state title when user has no playlists"
+ },
+ "collectionNoPlaylistsSubtitle": "Создайте плейлист, чтобы начать классифицировать треки",
+ "@collectionNoPlaylistsSubtitle": {
+ "description": "Empty state subtitle when user has no playlists"
+ },
+ "collectionPlaylistTracks": "{count, plural, one {{count} трек} few {{count} трека} many {{count} треков} other {{count} треков}}",
+ "@collectionPlaylistTracks": {
+ "description": "Track count label for custom playlists",
+ "placeholders": {
+ "count": {
+ "type": "int"
+ }
+ }
+ },
+ "collectionAddedToPlaylist": "Добавлено в \"{playlistName}\"",
+ "@collectionAddedToPlaylist": {
+ "description": "Snackbar after adding track to playlist",
+ "placeholders": {
+ "playlistName": {
+ "type": "String"
+ }
+ }
+ },
+ "collectionAlreadyInPlaylist": "Уже в \"{playlistName}\"",
+ "@collectionAlreadyInPlaylist": {
+ "description": "Snackbar when track already exists in playlist",
+ "placeholders": {
+ "playlistName": {
+ "type": "String"
+ }
+ }
+ },
+ "collectionPlaylistCreated": "Плейлист создан",
+ "@collectionPlaylistCreated": {
+ "description": "Snackbar after creating playlist"
+ },
+ "collectionPlaylistNameHint": "Название плейлиста",
+ "@collectionPlaylistNameHint": {
+ "description": "Hint text for playlist name input"
+ },
+ "collectionPlaylistNameRequired": "Имя плейлиста обязательно",
+ "@collectionPlaylistNameRequired": {
+ "description": "Validation error for empty playlist name"
+ },
+ "collectionRenamePlaylist": "Переименовать плейлист",
+ "@collectionRenamePlaylist": {
+ "description": "Action to rename playlist"
+ },
+ "collectionDeletePlaylist": "Удалить плейлист",
+ "@collectionDeletePlaylist": {
+ "description": "Action to delete playlist"
+ },
+ "collectionDeletePlaylistMessage": "Удалить \"{playlistName}\" и все треки внутри него?",
+ "@collectionDeletePlaylistMessage": {
+ "description": "Confirmation message for deleting playlist",
+ "placeholders": {
+ "playlistName": {
+ "type": "String"
+ }
+ }
+ },
+ "collectionPlaylistDeleted": "Плейлист удалён",
+ "@collectionPlaylistDeleted": {
+ "description": "Snackbar after deleting playlist"
+ },
+ "collectionPlaylistRenamed": "Плейлист переименован",
+ "@collectionPlaylistRenamed": {
+ "description": "Snackbar after renaming playlist"
+ },
+ "collectionWishlistEmptyTitle": "Список желаний пуст",
+ "@collectionWishlistEmptyTitle": {
+ "description": "Wishlist empty state title"
+ },
+ "collectionWishlistEmptySubtitle": "Нажмите + на треках, чтобы сохранить то, что вы хотите скачать позже",
+ "@collectionWishlistEmptySubtitle": {
+ "description": "Wishlist empty state subtitle"
+ },
+ "collectionLovedEmptyTitle": "Папка Любимые пуста",
+ "@collectionLovedEmptyTitle": {
+ "description": "Loved empty state title"
+ },
+ "collectionLovedEmptySubtitle": "Нажмите \"любовь\" на треках, чтобы сохранить ваши избранные",
+ "@collectionLovedEmptySubtitle": {
+ "description": "Loved empty state subtitle"
+ },
+ "collectionPlaylistEmptyTitle": "Плейлист пуст",
+ "@collectionPlaylistEmptyTitle": {
+ "description": "Playlist empty state title"
+ },
+ "collectionPlaylistEmptySubtitle": "Удерживайте + на любом треке, чтобы добавить его сюда",
+ "@collectionPlaylistEmptySubtitle": {
+ "description": "Playlist empty state subtitle"
+ },
+ "collectionRemoveFromPlaylist": "Удалить из плейлиста",
+ "@collectionRemoveFromPlaylist": {
+ "description": "Tooltip for removing track from playlist"
+ },
+ "collectionRemoveFromFolder": "Убрать из папки",
+ "@collectionRemoveFromFolder": {
+ "description": "Tooltip for removing track from wishlist/loved folder"
+ },
+ "collectionRemoved": "\"{trackName}\" удалён",
+ "@collectionRemoved": {
+ "description": "Snackbar after removing a track from a collection",
+ "placeholders": {
+ "trackName": {
+ "type": "String"
+ }
+ }
+ },
+ "collectionAddedToLoved": "\"{trackName}\" добавлен в Любимые",
+ "@collectionAddedToLoved": {
+ "description": "Snackbar after adding track to loved folder",
+ "placeholders": {
+ "trackName": {
+ "type": "String"
+ }
+ }
+ },
+ "collectionRemovedFromLoved": "\"{trackName}\" удалено из Любимых",
+ "@collectionRemovedFromLoved": {
+ "description": "Snackbar after removing track from loved folder",
+ "placeholders": {
+ "trackName": {
+ "type": "String"
+ }
+ }
+ },
+ "collectionAddedToWishlist": "\"{trackName}\" добавлен в список желаний",
+ "@collectionAddedToWishlist": {
+ "description": "Snackbar after adding track to wishlist",
+ "placeholders": {
+ "trackName": {
+ "type": "String"
+ }
+ }
+ },
+ "collectionRemovedFromWishlist": "\"{trackName}\" удалён из списка желаний",
+ "@collectionRemovedFromWishlist": {
+ "description": "Snackbar after removing track from wishlist",
+ "placeholders": {
+ "trackName": {
+ "type": "String"
+ }
+ }
+ },
+ "trackOptionAddToLoved": "Добавить в Любимое",
+ "@trackOptionAddToLoved": {
+ "description": "Bottom sheet action label - add track to loved folder"
+ },
+ "trackOptionRemoveFromLoved": "Исключить из Любимых",
+ "@trackOptionRemoveFromLoved": {
+ "description": "Bottom sheet action label - remove track from loved folder"
+ },
+ "trackOptionAddToWishlist": "Добавить в список желаний",
+ "@trackOptionAddToWishlist": {
+ "description": "Bottom sheet action label - add track to wishlist"
+ },
+ "trackOptionRemoveFromWishlist": "Удалить из списка желаний",
+ "@trackOptionRemoveFromWishlist": {
+ "description": "Bottom sheet action label - remove track from wishlist"
+ },
+ "collectionPlaylistChangeCover": "Изменить обложку",
+ "@collectionPlaylistChangeCover": {
+ "description": "Bottom sheet action to pick a custom cover image for a playlist"
+ },
+ "collectionPlaylistRemoveCover": "Удалить обложку",
+ "@collectionPlaylistRemoveCover": {
+ "description": "Bottom sheet action to remove custom cover image from a playlist"
+ },
+ "selectionShareCount": "Отправить {count} {count, plural, one {трек} few {трека} many {треков} other{треков}}",
+ "@selectionShareCount": {
+ "description": "Share button text with count in selection mode",
+ "placeholders": {
+ "count": {
+ "type": "int"
+ }
+ }
+ },
+ "selectionShareNoFiles": "No shareable files found",
+ "@selectionShareNoFiles": {
+ "description": "Snackbar when no selected files exist on disk"
+ },
+ "selectionConvertCount": "Конвертировать {count} {count, plural, one {трек} few {трека} many {треков} other{треков}}",
+ "@selectionConvertCount": {
+ "description": "Convert button text with count in selection mode",
+ "placeholders": {
+ "count": {
+ "type": "int"
+ }
+ }
+ },
+ "selectionConvertNoConvertible": "Не выбраны конвертируемые треки",
+ "@selectionConvertNoConvertible": {
+ "description": "Snackbar when no selected tracks support conversion"
+ },
+ "selectionBatchConvertConfirmTitle": "Пакетная конвертация",
+ "@selectionBatchConvertConfirmTitle": {
+ "description": "Confirmation dialog title for batch conversion"
+ },
+ "selectionBatchConvertConfirmMessage": "Convert {count} {count, plural, =1{track} other{tracks}} to {format} at {bitrate}?\n\nOriginal files will be deleted after conversion.",
+ "@selectionBatchConvertConfirmMessage": {
+ "description": "Confirmation dialog message for batch conversion",
+ "placeholders": {
+ "count": {
+ "type": "int"
+ },
+ "format": {
+ "type": "String"
+ },
+ "bitrate": {
+ "type": "String"
+ }
+ }
+ },
+ "selectionBatchConvertProgress": "Конвертация {current} из {total}...",
+ "@selectionBatchConvertProgress": {
+ "description": "Snackbar during batch conversion progress",
+ "placeholders": {
+ "current": {
+ "type": "int"
+ },
+ "total": {
+ "type": "int"
+ }
+ }
+ },
+ "selectionBatchConvertSuccess": "Конвертировано {success} треков {total} в {format}",
+ "@selectionBatchConvertSuccess": {
+ "description": "Snackbar after batch conversion completes",
+ "placeholders": {
+ "success": {
+ "type": "int"
+ },
+ "total": {
+ "type": "int"
+ },
+ "format": {
+ "type": "String"
+ }
+ }
+ },
"downloadedAlbumDownloadedCount": "{count} скачано",
"@downloadedAlbumDownloadedCount": {
"description": "Downloaded tracks count badge",
@@ -2792,7 +3094,7 @@
}
}
},
- "downloadUseAlbumArtistForFoldersAlbumSubtitle": "Artist folders use Album Artist when available",
+ "downloadUseAlbumArtistForFoldersAlbumSubtitle": "Для папок исполнителей используется исполнитель альбома, если он указан",
"@downloadUseAlbumArtistForFoldersAlbumSubtitle": {
"description": "Subtitle when Album Artist is used for folder naming"
},
@@ -2800,4 +3102,4 @@
"@downloadUseAlbumArtistForFoldersTrackSubtitle": {
"description": "Subtitle when Track Artist is used for folder naming"
}
-}
+}
\ No newline at end of file
diff --git a/lib/l10n/arb/app_tr.arb b/lib/l10n/arb/app_tr.arb
index b7b16c7f..7b808ec4 100644
--- a/lib/l10n/arb/app_tr.arb
+++ b/lib/l10n/arb/app_tr.arb
@@ -450,7 +450,7 @@
"@aboutSpotiSaverDesc": {
"description": "Credit for SpotiSaver API"
},
- "aboutAppDescription": "Spotify şarkılarını Tidal, Qobuz ve Amazon Music'den yüksek kalitede indir.",
+ "aboutAppDescription": "Spotify şarkılarını Tidal ve Qobuz'den yüksek kalitede indir.",
"@aboutAppDescription": {
"description": "App description in header card"
},
@@ -1089,7 +1089,7 @@
},
"providerBuiltIn": "Dahili",
"@providerBuiltIn": {
- "description": "Label for built-in providers (Tidal/Qobuz/Amazon)"
+ "description": "Label for built-in providers (Tidal/Qobuz)"
},
"providerExtension": "Eklenti",
"@providerExtension": {
@@ -2358,7 +2358,7 @@
"@tutorialWelcomeTip1": {
"description": "Tutorial welcome tip 1"
},
- "tutorialWelcomeTip2": "Get FLAC quality audio from Tidal, Qobuz, or Amazon Music",
+ "tutorialWelcomeTip2": "Tidal, Qobuz veya Deezer'den FLAC kalitesinde ses alın",
"@tutorialWelcomeTip2": {
"description": "Tutorial welcome tip 2"
},
diff --git a/lib/l10n/arb/app_zh.arb b/lib/l10n/arb/app_zh.arb
index f6f4895f..453c9f5c 100644
--- a/lib/l10n/arb/app_zh.arb
+++ b/lib/l10n/arb/app_zh.arb
@@ -402,7 +402,7 @@
"@aboutDabMusicDesc": {
"description": "Credit for DAB Music API"
},
- "aboutAppDescription": "Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.",
+ "aboutAppDescription": "Download Spotify tracks in lossless quality from Tidal and Qobuz.",
"@aboutAppDescription": {
"description": "App description in header card"
},
@@ -1005,7 +1005,7 @@
},
"providerBuiltIn": "Built-in",
"@providerBuiltIn": {
- "description": "Label for built-in providers (Tidal/Qobuz/Amazon)"
+ "description": "Label for built-in providers (Tidal/Qobuz)"
},
"providerExtension": "Extension",
"@providerExtension": {
diff --git a/lib/l10n/arb/app_zh_CN.arb b/lib/l10n/arb/app_zh_CN.arb
index 67d8b58c..d7a9c1aa 100644
--- a/lib/l10n/arb/app_zh_CN.arb
+++ b/lib/l10n/arb/app_zh_CN.arb
@@ -991,6 +991,14 @@
"@filenameFormat": {
"description": "Setting title - filename pattern"
},
+ "filenameShowAdvancedTags": "Show advanced tags",
+ "@filenameShowAdvancedTags": {
+ "description": "Toggle label for showing advanced filename tags"
+ },
+ "filenameShowAdvancedTagsDescription": "Enable formatted tags for track padding and date patterns",
+ "@filenameShowAdvancedTagsDescription": {
+ "description": "Description for advanced filename tag toggle"
+ },
"folderOrganizationNone": "No organization",
"@folderOrganizationNone": {
"description": "Folder option - flat structure"
@@ -1749,6 +1757,14 @@
"@youtubeQualityNote": {
"description": "Note for YouTube service explaining lossy-only quality"
},
+ "youtubeOpusBitrateTitle": "YouTube Opus Bitrate",
+ "@youtubeOpusBitrateTitle": {
+ "description": "Title for YouTube Opus bitrate setting"
+ },
+ "youtubeMp3BitrateTitle": "YouTube MP3 Bitrate",
+ "@youtubeMp3BitrateTitle": {
+ "description": "Title for YouTube MP3 bitrate setting"
+ },
"downloadAskBeforeDownload": "Ask Before Download",
"@downloadAskBeforeDownload": {
"description": "Setting - show quality picker"
@@ -2198,6 +2214,15 @@
"@libraryAboutDescription": {
"description": "Description of local library feature"
},
+ "libraryTracksUnit": "{count, plural, =1{track} other{tracks}}",
+ "@libraryTracksUnit": {
+ "description": "Unit label for tracks count (without the number itself)",
+ "placeholders": {
+ "count": {
+ "type": "int"
+ }
+ }
+ },
"libraryLastScanned": "Last scanned: {time}",
"@libraryLastScanned": {
"description": "Last scan time display",
@@ -2783,6 +2808,283 @@
"@trackConvertFailed": {
"description": "Snackbar when conversion fails"
},
+ "actionCreate": "Create",
+ "@actionCreate": {
+ "description": "Generic action button - create"
+ },
+ "collectionFoldersTitle": "My folders",
+ "@collectionFoldersTitle": {
+ "description": "Library section title for custom folders"
+ },
+ "collectionWishlist": "Wishlist",
+ "@collectionWishlist": {
+ "description": "Custom folder for saved tracks to download later"
+ },
+ "collectionLoved": "Loved",
+ "@collectionLoved": {
+ "description": "Custom folder for favorite tracks"
+ },
+ "collectionPlaylists": "Playlists",
+ "@collectionPlaylists": {
+ "description": "Custom user playlists folder"
+ },
+ "collectionPlaylist": "Playlist",
+ "@collectionPlaylist": {
+ "description": "Single playlist label"
+ },
+ "collectionAddToPlaylist": "Add to playlist",
+ "@collectionAddToPlaylist": {
+ "description": "Action to add a track to user playlist"
+ },
+ "collectionCreatePlaylist": "Create playlist",
+ "@collectionCreatePlaylist": {
+ "description": "Action to create a new playlist"
+ },
+ "collectionNoPlaylistsYet": "No playlists yet",
+ "@collectionNoPlaylistsYet": {
+ "description": "Empty state title when user has no playlists"
+ },
+ "collectionNoPlaylistsSubtitle": "Create a playlist to start categorizing tracks",
+ "@collectionNoPlaylistsSubtitle": {
+ "description": "Empty state subtitle when user has no playlists"
+ },
+ "collectionPlaylistTracks": "{count, plural, =1{1 track} other{{count} tracks}}",
+ "@collectionPlaylistTracks": {
+ "description": "Track count label for custom playlists",
+ "placeholders": {
+ "count": {
+ "type": "int"
+ }
+ }
+ },
+ "collectionAddedToPlaylist": "Added to \"{playlistName}\"",
+ "@collectionAddedToPlaylist": {
+ "description": "Snackbar after adding track to playlist",
+ "placeholders": {
+ "playlistName": {
+ "type": "String"
+ }
+ }
+ },
+ "collectionAlreadyInPlaylist": "Already in \"{playlistName}\"",
+ "@collectionAlreadyInPlaylist": {
+ "description": "Snackbar when track already exists in playlist",
+ "placeholders": {
+ "playlistName": {
+ "type": "String"
+ }
+ }
+ },
+ "collectionPlaylistCreated": "Playlist created",
+ "@collectionPlaylistCreated": {
+ "description": "Snackbar after creating playlist"
+ },
+ "collectionPlaylistNameHint": "Playlist name",
+ "@collectionPlaylistNameHint": {
+ "description": "Hint text for playlist name input"
+ },
+ "collectionPlaylistNameRequired": "Playlist name is required",
+ "@collectionPlaylistNameRequired": {
+ "description": "Validation error for empty playlist name"
+ },
+ "collectionRenamePlaylist": "Rename playlist",
+ "@collectionRenamePlaylist": {
+ "description": "Action to rename playlist"
+ },
+ "collectionDeletePlaylist": "Delete playlist",
+ "@collectionDeletePlaylist": {
+ "description": "Action to delete playlist"
+ },
+ "collectionDeletePlaylistMessage": "Delete \"{playlistName}\" and all tracks inside it?",
+ "@collectionDeletePlaylistMessage": {
+ "description": "Confirmation message for deleting playlist",
+ "placeholders": {
+ "playlistName": {
+ "type": "String"
+ }
+ }
+ },
+ "collectionPlaylistDeleted": "Playlist deleted",
+ "@collectionPlaylistDeleted": {
+ "description": "Snackbar after deleting playlist"
+ },
+ "collectionPlaylistRenamed": "Playlist renamed",
+ "@collectionPlaylistRenamed": {
+ "description": "Snackbar after renaming playlist"
+ },
+ "collectionWishlistEmptyTitle": "Wishlist is empty",
+ "@collectionWishlistEmptyTitle": {
+ "description": "Wishlist empty state title"
+ },
+ "collectionWishlistEmptySubtitle": "Tap + on tracks to save what you want to download later",
+ "@collectionWishlistEmptySubtitle": {
+ "description": "Wishlist empty state subtitle"
+ },
+ "collectionLovedEmptyTitle": "Loved folder is empty",
+ "@collectionLovedEmptyTitle": {
+ "description": "Loved empty state title"
+ },
+ "collectionLovedEmptySubtitle": "Tap love on tracks to keep your favorites",
+ "@collectionLovedEmptySubtitle": {
+ "description": "Loved empty state subtitle"
+ },
+ "collectionPlaylistEmptyTitle": "Playlist is empty",
+ "@collectionPlaylistEmptyTitle": {
+ "description": "Playlist empty state title"
+ },
+ "collectionPlaylistEmptySubtitle": "Long-press + on any track to add it here",
+ "@collectionPlaylistEmptySubtitle": {
+ "description": "Playlist empty state subtitle"
+ },
+ "collectionRemoveFromPlaylist": "Remove from playlist",
+ "@collectionRemoveFromPlaylist": {
+ "description": "Tooltip for removing track from playlist"
+ },
+ "collectionRemoveFromFolder": "Remove from folder",
+ "@collectionRemoveFromFolder": {
+ "description": "Tooltip for removing track from wishlist/loved folder"
+ },
+ "collectionRemoved": "\"{trackName}\" removed",
+ "@collectionRemoved": {
+ "description": "Snackbar after removing a track from a collection",
+ "placeholders": {
+ "trackName": {
+ "type": "String"
+ }
+ }
+ },
+ "collectionAddedToLoved": "\"{trackName}\" added to Loved",
+ "@collectionAddedToLoved": {
+ "description": "Snackbar after adding track to loved folder",
+ "placeholders": {
+ "trackName": {
+ "type": "String"
+ }
+ }
+ },
+ "collectionRemovedFromLoved": "\"{trackName}\" removed from Loved",
+ "@collectionRemovedFromLoved": {
+ "description": "Snackbar after removing track from loved folder",
+ "placeholders": {
+ "trackName": {
+ "type": "String"
+ }
+ }
+ },
+ "collectionAddedToWishlist": "\"{trackName}\" added to Wishlist",
+ "@collectionAddedToWishlist": {
+ "description": "Snackbar after adding track to wishlist",
+ "placeholders": {
+ "trackName": {
+ "type": "String"
+ }
+ }
+ },
+ "collectionRemovedFromWishlist": "\"{trackName}\" removed from Wishlist",
+ "@collectionRemovedFromWishlist": {
+ "description": "Snackbar after removing track from wishlist",
+ "placeholders": {
+ "trackName": {
+ "type": "String"
+ }
+ }
+ },
+ "trackOptionAddToLoved": "Add to Loved",
+ "@trackOptionAddToLoved": {
+ "description": "Bottom sheet action label - add track to loved folder"
+ },
+ "trackOptionRemoveFromLoved": "Remove from Loved",
+ "@trackOptionRemoveFromLoved": {
+ "description": "Bottom sheet action label - remove track from loved folder"
+ },
+ "trackOptionAddToWishlist": "Add to Wishlist",
+ "@trackOptionAddToWishlist": {
+ "description": "Bottom sheet action label - add track to wishlist"
+ },
+ "trackOptionRemoveFromWishlist": "Remove from Wishlist",
+ "@trackOptionRemoveFromWishlist": {
+ "description": "Bottom sheet action label - remove track from wishlist"
+ },
+ "collectionPlaylistChangeCover": "Change cover image",
+ "@collectionPlaylistChangeCover": {
+ "description": "Bottom sheet action to pick a custom cover image for a playlist"
+ },
+ "collectionPlaylistRemoveCover": "Remove cover image",
+ "@collectionPlaylistRemoveCover": {
+ "description": "Bottom sheet action to remove custom cover image from a playlist"
+ },
+ "selectionShareCount": "Share {count} {count, plural, =1{track} other{tracks}}",
+ "@selectionShareCount": {
+ "description": "Share button text with count in selection mode",
+ "placeholders": {
+ "count": {
+ "type": "int"
+ }
+ }
+ },
+ "selectionShareNoFiles": "No shareable files found",
+ "@selectionShareNoFiles": {
+ "description": "Snackbar when no selected files exist on disk"
+ },
+ "selectionConvertCount": "Convert {count} {count, plural, =1{track} other{tracks}}",
+ "@selectionConvertCount": {
+ "description": "Convert button text with count in selection mode",
+ "placeholders": {
+ "count": {
+ "type": "int"
+ }
+ }
+ },
+ "selectionConvertNoConvertible": "No convertible tracks selected",
+ "@selectionConvertNoConvertible": {
+ "description": "Snackbar when no selected tracks support conversion"
+ },
+ "selectionBatchConvertConfirmTitle": "Batch Convert",
+ "@selectionBatchConvertConfirmTitle": {
+ "description": "Confirmation dialog title for batch conversion"
+ },
+ "selectionBatchConvertConfirmMessage": "Convert {count} {count, plural, =1{track} other{tracks}} to {format} at {bitrate}?\n\nOriginal files will be deleted after conversion.",
+ "@selectionBatchConvertConfirmMessage": {
+ "description": "Confirmation dialog message for batch conversion",
+ "placeholders": {
+ "count": {
+ "type": "int"
+ },
+ "format": {
+ "type": "String"
+ },
+ "bitrate": {
+ "type": "String"
+ }
+ }
+ },
+ "selectionBatchConvertProgress": "Converting {current} of {total}...",
+ "@selectionBatchConvertProgress": {
+ "description": "Snackbar during batch conversion progress",
+ "placeholders": {
+ "current": {
+ "type": "int"
+ },
+ "total": {
+ "type": "int"
+ }
+ }
+ },
+ "selectionBatchConvertSuccess": "Converted {success} of {total} tracks to {format}",
+ "@selectionBatchConvertSuccess": {
+ "description": "Snackbar after batch conversion completes",
+ "placeholders": {
+ "success": {
+ "type": "int"
+ },
+ "total": {
+ "type": "int"
+ },
+ "format": {
+ "type": "String"
+ }
+ }
+ },
"downloadedAlbumDownloadedCount": "{count} downloaded",
"@downloadedAlbumDownloadedCount": {
"description": "Downloaded tracks count badge",
@@ -2800,4 +3102,4 @@
"@downloadUseAlbumArtistForFoldersTrackSubtitle": {
"description": "Subtitle when Track Artist is used for folder naming"
}
-}
+}
\ No newline at end of file
diff --git a/lib/l10n/arb/app_zh_TW.arb b/lib/l10n/arb/app_zh_TW.arb
index d230aa50..b51686c0 100644
--- a/lib/l10n/arb/app_zh_TW.arb
+++ b/lib/l10n/arb/app_zh_TW.arb
@@ -991,6 +991,14 @@
"@filenameFormat": {
"description": "Setting title - filename pattern"
},
+ "filenameShowAdvancedTags": "Show advanced tags",
+ "@filenameShowAdvancedTags": {
+ "description": "Toggle label for showing advanced filename tags"
+ },
+ "filenameShowAdvancedTagsDescription": "Enable formatted tags for track padding and date patterns",
+ "@filenameShowAdvancedTagsDescription": {
+ "description": "Description for advanced filename tag toggle"
+ },
"folderOrganizationNone": "No organization",
"@folderOrganizationNone": {
"description": "Folder option - flat structure"
@@ -1749,6 +1757,14 @@
"@youtubeQualityNote": {
"description": "Note for YouTube service explaining lossy-only quality"
},
+ "youtubeOpusBitrateTitle": "YouTube Opus Bitrate",
+ "@youtubeOpusBitrateTitle": {
+ "description": "Title for YouTube Opus bitrate setting"
+ },
+ "youtubeMp3BitrateTitle": "YouTube MP3 Bitrate",
+ "@youtubeMp3BitrateTitle": {
+ "description": "Title for YouTube MP3 bitrate setting"
+ },
"downloadAskBeforeDownload": "Ask Before Download",
"@downloadAskBeforeDownload": {
"description": "Setting - show quality picker"
@@ -2198,6 +2214,15 @@
"@libraryAboutDescription": {
"description": "Description of local library feature"
},
+ "libraryTracksUnit": "{count, plural, =1{track} other{tracks}}",
+ "@libraryTracksUnit": {
+ "description": "Unit label for tracks count (without the number itself)",
+ "placeholders": {
+ "count": {
+ "type": "int"
+ }
+ }
+ },
"libraryLastScanned": "Last scanned: {time}",
"@libraryLastScanned": {
"description": "Last scan time display",
@@ -2783,6 +2808,283 @@
"@trackConvertFailed": {
"description": "Snackbar when conversion fails"
},
+ "actionCreate": "Create",
+ "@actionCreate": {
+ "description": "Generic action button - create"
+ },
+ "collectionFoldersTitle": "My folders",
+ "@collectionFoldersTitle": {
+ "description": "Library section title for custom folders"
+ },
+ "collectionWishlist": "Wishlist",
+ "@collectionWishlist": {
+ "description": "Custom folder for saved tracks to download later"
+ },
+ "collectionLoved": "Loved",
+ "@collectionLoved": {
+ "description": "Custom folder for favorite tracks"
+ },
+ "collectionPlaylists": "Playlists",
+ "@collectionPlaylists": {
+ "description": "Custom user playlists folder"
+ },
+ "collectionPlaylist": "Playlist",
+ "@collectionPlaylist": {
+ "description": "Single playlist label"
+ },
+ "collectionAddToPlaylist": "Add to playlist",
+ "@collectionAddToPlaylist": {
+ "description": "Action to add a track to user playlist"
+ },
+ "collectionCreatePlaylist": "Create playlist",
+ "@collectionCreatePlaylist": {
+ "description": "Action to create a new playlist"
+ },
+ "collectionNoPlaylistsYet": "No playlists yet",
+ "@collectionNoPlaylistsYet": {
+ "description": "Empty state title when user has no playlists"
+ },
+ "collectionNoPlaylistsSubtitle": "Create a playlist to start categorizing tracks",
+ "@collectionNoPlaylistsSubtitle": {
+ "description": "Empty state subtitle when user has no playlists"
+ },
+ "collectionPlaylistTracks": "{count, plural, =1{1 track} other{{count} tracks}}",
+ "@collectionPlaylistTracks": {
+ "description": "Track count label for custom playlists",
+ "placeholders": {
+ "count": {
+ "type": "int"
+ }
+ }
+ },
+ "collectionAddedToPlaylist": "Added to \"{playlistName}\"",
+ "@collectionAddedToPlaylist": {
+ "description": "Snackbar after adding track to playlist",
+ "placeholders": {
+ "playlistName": {
+ "type": "String"
+ }
+ }
+ },
+ "collectionAlreadyInPlaylist": "Already in \"{playlistName}\"",
+ "@collectionAlreadyInPlaylist": {
+ "description": "Snackbar when track already exists in playlist",
+ "placeholders": {
+ "playlistName": {
+ "type": "String"
+ }
+ }
+ },
+ "collectionPlaylistCreated": "Playlist created",
+ "@collectionPlaylistCreated": {
+ "description": "Snackbar after creating playlist"
+ },
+ "collectionPlaylistNameHint": "Playlist name",
+ "@collectionPlaylistNameHint": {
+ "description": "Hint text for playlist name input"
+ },
+ "collectionPlaylistNameRequired": "Playlist name is required",
+ "@collectionPlaylistNameRequired": {
+ "description": "Validation error for empty playlist name"
+ },
+ "collectionRenamePlaylist": "Rename playlist",
+ "@collectionRenamePlaylist": {
+ "description": "Action to rename playlist"
+ },
+ "collectionDeletePlaylist": "Delete playlist",
+ "@collectionDeletePlaylist": {
+ "description": "Action to delete playlist"
+ },
+ "collectionDeletePlaylistMessage": "Delete \"{playlistName}\" and all tracks inside it?",
+ "@collectionDeletePlaylistMessage": {
+ "description": "Confirmation message for deleting playlist",
+ "placeholders": {
+ "playlistName": {
+ "type": "String"
+ }
+ }
+ },
+ "collectionPlaylistDeleted": "Playlist deleted",
+ "@collectionPlaylistDeleted": {
+ "description": "Snackbar after deleting playlist"
+ },
+ "collectionPlaylistRenamed": "Playlist renamed",
+ "@collectionPlaylistRenamed": {
+ "description": "Snackbar after renaming playlist"
+ },
+ "collectionWishlistEmptyTitle": "Wishlist is empty",
+ "@collectionWishlistEmptyTitle": {
+ "description": "Wishlist empty state title"
+ },
+ "collectionWishlistEmptySubtitle": "Tap + on tracks to save what you want to download later",
+ "@collectionWishlistEmptySubtitle": {
+ "description": "Wishlist empty state subtitle"
+ },
+ "collectionLovedEmptyTitle": "Loved folder is empty",
+ "@collectionLovedEmptyTitle": {
+ "description": "Loved empty state title"
+ },
+ "collectionLovedEmptySubtitle": "Tap love on tracks to keep your favorites",
+ "@collectionLovedEmptySubtitle": {
+ "description": "Loved empty state subtitle"
+ },
+ "collectionPlaylistEmptyTitle": "Playlist is empty",
+ "@collectionPlaylistEmptyTitle": {
+ "description": "Playlist empty state title"
+ },
+ "collectionPlaylistEmptySubtitle": "Long-press + on any track to add it here",
+ "@collectionPlaylistEmptySubtitle": {
+ "description": "Playlist empty state subtitle"
+ },
+ "collectionRemoveFromPlaylist": "Remove from playlist",
+ "@collectionRemoveFromPlaylist": {
+ "description": "Tooltip for removing track from playlist"
+ },
+ "collectionRemoveFromFolder": "Remove from folder",
+ "@collectionRemoveFromFolder": {
+ "description": "Tooltip for removing track from wishlist/loved folder"
+ },
+ "collectionRemoved": "\"{trackName}\" removed",
+ "@collectionRemoved": {
+ "description": "Snackbar after removing a track from a collection",
+ "placeholders": {
+ "trackName": {
+ "type": "String"
+ }
+ }
+ },
+ "collectionAddedToLoved": "\"{trackName}\" added to Loved",
+ "@collectionAddedToLoved": {
+ "description": "Snackbar after adding track to loved folder",
+ "placeholders": {
+ "trackName": {
+ "type": "String"
+ }
+ }
+ },
+ "collectionRemovedFromLoved": "\"{trackName}\" removed from Loved",
+ "@collectionRemovedFromLoved": {
+ "description": "Snackbar after removing track from loved folder",
+ "placeholders": {
+ "trackName": {
+ "type": "String"
+ }
+ }
+ },
+ "collectionAddedToWishlist": "\"{trackName}\" added to Wishlist",
+ "@collectionAddedToWishlist": {
+ "description": "Snackbar after adding track to wishlist",
+ "placeholders": {
+ "trackName": {
+ "type": "String"
+ }
+ }
+ },
+ "collectionRemovedFromWishlist": "\"{trackName}\" removed from Wishlist",
+ "@collectionRemovedFromWishlist": {
+ "description": "Snackbar after removing track from wishlist",
+ "placeholders": {
+ "trackName": {
+ "type": "String"
+ }
+ }
+ },
+ "trackOptionAddToLoved": "Add to Loved",
+ "@trackOptionAddToLoved": {
+ "description": "Bottom sheet action label - add track to loved folder"
+ },
+ "trackOptionRemoveFromLoved": "Remove from Loved",
+ "@trackOptionRemoveFromLoved": {
+ "description": "Bottom sheet action label - remove track from loved folder"
+ },
+ "trackOptionAddToWishlist": "Add to Wishlist",
+ "@trackOptionAddToWishlist": {
+ "description": "Bottom sheet action label - add track to wishlist"
+ },
+ "trackOptionRemoveFromWishlist": "Remove from Wishlist",
+ "@trackOptionRemoveFromWishlist": {
+ "description": "Bottom sheet action label - remove track from wishlist"
+ },
+ "collectionPlaylistChangeCover": "Change cover image",
+ "@collectionPlaylistChangeCover": {
+ "description": "Bottom sheet action to pick a custom cover image for a playlist"
+ },
+ "collectionPlaylistRemoveCover": "Remove cover image",
+ "@collectionPlaylistRemoveCover": {
+ "description": "Bottom sheet action to remove custom cover image from a playlist"
+ },
+ "selectionShareCount": "Share {count} {count, plural, =1{track} other{tracks}}",
+ "@selectionShareCount": {
+ "description": "Share button text with count in selection mode",
+ "placeholders": {
+ "count": {
+ "type": "int"
+ }
+ }
+ },
+ "selectionShareNoFiles": "No shareable files found",
+ "@selectionShareNoFiles": {
+ "description": "Snackbar when no selected files exist on disk"
+ },
+ "selectionConvertCount": "Convert {count} {count, plural, =1{track} other{tracks}}",
+ "@selectionConvertCount": {
+ "description": "Convert button text with count in selection mode",
+ "placeholders": {
+ "count": {
+ "type": "int"
+ }
+ }
+ },
+ "selectionConvertNoConvertible": "No convertible tracks selected",
+ "@selectionConvertNoConvertible": {
+ "description": "Snackbar when no selected tracks support conversion"
+ },
+ "selectionBatchConvertConfirmTitle": "Batch Convert",
+ "@selectionBatchConvertConfirmTitle": {
+ "description": "Confirmation dialog title for batch conversion"
+ },
+ "selectionBatchConvertConfirmMessage": "Convert {count} {count, plural, =1{track} other{tracks}} to {format} at {bitrate}?\n\nOriginal files will be deleted after conversion.",
+ "@selectionBatchConvertConfirmMessage": {
+ "description": "Confirmation dialog message for batch conversion",
+ "placeholders": {
+ "count": {
+ "type": "int"
+ },
+ "format": {
+ "type": "String"
+ },
+ "bitrate": {
+ "type": "String"
+ }
+ }
+ },
+ "selectionBatchConvertProgress": "Converting {current} of {total}...",
+ "@selectionBatchConvertProgress": {
+ "description": "Snackbar during batch conversion progress",
+ "placeholders": {
+ "current": {
+ "type": "int"
+ },
+ "total": {
+ "type": "int"
+ }
+ }
+ },
+ "selectionBatchConvertSuccess": "Converted {success} of {total} tracks to {format}",
+ "@selectionBatchConvertSuccess": {
+ "description": "Snackbar after batch conversion completes",
+ "placeholders": {
+ "success": {
+ "type": "int"
+ },
+ "total": {
+ "type": "int"
+ },
+ "format": {
+ "type": "String"
+ }
+ }
+ },
"downloadedAlbumDownloadedCount": "{count} downloaded",
"@downloadedAlbumDownloadedCount": {
"description": "Downloaded tracks count badge",
@@ -2800,4 +3102,4 @@
"@downloadUseAlbumArtistForFoldersTrackSubtitle": {
"description": "Subtitle when Track Artist is used for folder naming"
}
-}
+}
\ No newline at end of file
diff --git a/lib/models/download_item.dart b/lib/models/download_item.dart
index 4d8a650e..db55029b 100644
--- a/lib/models/download_item.dart
+++ b/lib/models/download_item.dart
@@ -34,6 +34,7 @@ class DownloadItem {
final DownloadErrorType? errorType;
final DateTime createdAt;
final String? qualityOverride; // Override quality for this specific download
+ final String? playlistName; // Playlist context for folder organization
const DownloadItem({
required this.id,
@@ -48,6 +49,7 @@ class DownloadItem {
this.errorType,
required this.createdAt,
this.qualityOverride,
+ this.playlistName,
});
DownloadItem copyWith({
@@ -63,6 +65,7 @@ class DownloadItem {
DownloadErrorType? errorType,
DateTime? createdAt,
String? qualityOverride,
+ String? playlistName,
}) {
return DownloadItem(
id: id ?? this.id,
@@ -77,6 +80,7 @@ class DownloadItem {
errorType: errorType ?? this.errorType,
createdAt: createdAt ?? this.createdAt,
qualityOverride: qualityOverride ?? this.qualityOverride,
+ playlistName: playlistName ?? this.playlistName,
);
}
diff --git a/lib/models/download_item.g.dart b/lib/models/download_item.g.dart
index 098290ce..961e6d6d 100644
--- a/lib/models/download_item.g.dart
+++ b/lib/models/download_item.g.dart
@@ -21,6 +21,7 @@ DownloadItem _$DownloadItemFromJson(Map json) => DownloadItem(
errorType: $enumDecodeNullable(_$DownloadErrorTypeEnumMap, json['errorType']),
createdAt: DateTime.parse(json['createdAt'] as String),
qualityOverride: json['qualityOverride'] as String?,
+ playlistName: json['playlistName'] as String?,
);
Map _$DownloadItemToJson(DownloadItem instance) =>
@@ -37,6 +38,7 @@ Map _$DownloadItemToJson(DownloadItem instance) =>
'errorType': _$DownloadErrorTypeEnumMap[instance.errorType],
'createdAt': instance.createdAt.toIso8601String(),
'qualityOverride': instance.qualityOverride,
+ 'playlistName': instance.playlistName,
};
const _$DownloadStatusEnumMap = {
diff --git a/lib/models/settings.dart b/lib/models/settings.dart
index 545e7440..67e3e9ac 100644
--- a/lib/models/settings.dart
+++ b/lib/models/settings.dart
@@ -55,17 +55,16 @@ class AppSettings {
final String
songLinkRegion; // SongLink userCountry region code used for platform lookup
- // Local Library Settings
final bool localLibraryEnabled; // Enable local library scanning
final String localLibraryPath; // Path to scan for audio files
+ final String
+ localLibraryBookmark; // Base64-encoded iOS security-scoped bookmark
final bool
localLibraryShowDuplicates; // Show indicator when searching for existing tracks
- // Tutorial/Onboarding
final bool
hasCompletedTutorial; // Track if user has completed the app tutorial
- // Lyrics Provider Settings
final List
lyricsProviders; // Ordered list of enabled lyrics provider IDs
final bool
@@ -77,7 +76,6 @@ class AppSettings {
final String
musixmatchLanguage; // Optional ISO language code for Musixmatch localized lyrics
- // Version upgrade tracking
final String
lastSeenVersion; // Last app version the user has acknowledged (e.g. '3.7.0')
@@ -106,7 +104,7 @@ class AppSettings {
this.askQualityBeforeDownload = true,
this.spotifyClientId = '',
this.spotifyClientSecret = '',
- this.useCustomSpotifyCredentials = true,
+ this.useCustomSpotifyCredentials = false,
this.metadataSource = 'deezer',
this.enableLogging = false,
this.useExtensionProviders = true,
@@ -124,13 +122,11 @@ class AppSettings {
this.downloadNetworkMode = 'any',
this.networkCompatibilityMode = false,
this.songLinkRegion = 'US',
- // Local Library defaults
this.localLibraryEnabled = false,
this.localLibraryPath = '',
+ this.localLibraryBookmark = '',
this.localLibraryShowDuplicates = true,
- // Tutorial default
this.hasCompletedTutorial = false,
- // Lyrics providers default order
this.lyricsProviders = const [
'lrclib',
'spotify_api',
@@ -143,7 +139,6 @@ class AppSettings {
this.lyricsIncludeRomanizationNetease = false,
this.lyricsMultiPersonWordByWord = false,
this.musixmatchLanguage = '',
- // Version upgrade tracking
this.lastSeenVersion = '',
});
@@ -154,7 +149,7 @@ class AppSettings {
String? downloadDirectory,
String? storageMode,
String? downloadTreeUri,
- bool? autoFallback,
+ bool? autoFallback,
bool? embedMetadata,
bool? embedLyrics,
bool? maxQualityCover,
@@ -191,19 +186,16 @@ class AppSettings {
String? downloadNetworkMode,
bool? networkCompatibilityMode,
String? songLinkRegion,
- // Local Library
bool? localLibraryEnabled,
String? localLibraryPath,
+ String? localLibraryBookmark,
bool? localLibraryShowDuplicates,
- // Tutorial
bool? hasCompletedTutorial,
- // Lyrics providers
List? lyricsProviders,
bool? lyricsIncludeTranslationNetease,
bool? lyricsIncludeRomanizationNetease,
bool? lyricsMultiPersonWordByWord,
String? musixmatchLanguage,
- // Version upgrade tracking
String? lastSeenVersion,
}) {
return AppSettings(
@@ -259,14 +251,12 @@ class AppSettings {
networkCompatibilityMode:
networkCompatibilityMode ?? this.networkCompatibilityMode,
songLinkRegion: songLinkRegion ?? this.songLinkRegion,
- // Local Library
localLibraryEnabled: localLibraryEnabled ?? this.localLibraryEnabled,
localLibraryPath: localLibraryPath ?? this.localLibraryPath,
+ localLibraryBookmark: localLibraryBookmark ?? this.localLibraryBookmark,
localLibraryShowDuplicates:
localLibraryShowDuplicates ?? this.localLibraryShowDuplicates,
- // Tutorial
hasCompletedTutorial: hasCompletedTutorial ?? this.hasCompletedTutorial,
- // Lyrics providers
lyricsProviders: lyricsProviders ?? this.lyricsProviders,
lyricsIncludeTranslationNetease:
lyricsIncludeTranslationNetease ??
@@ -277,7 +267,6 @@ class AppSettings {
lyricsMultiPersonWordByWord:
lyricsMultiPersonWordByWord ?? this.lyricsMultiPersonWordByWord,
musixmatchLanguage: musixmatchLanguage ?? this.musixmatchLanguage,
- // Version upgrade tracking
lastSeenVersion: lastSeenVersion ?? this.lastSeenVersion,
);
}
diff --git a/lib/models/settings.g.dart b/lib/models/settings.g.dart
index 933178e3..c3eecb50 100644
--- a/lib/models/settings.g.dart
+++ b/lib/models/settings.g.dart
@@ -33,7 +33,7 @@ AppSettings _$AppSettingsFromJson(Map json) => AppSettings(
spotifyClientId: json['spotifyClientId'] as String? ?? '',
spotifyClientSecret: json['spotifyClientSecret'] as String? ?? '',
useCustomSpotifyCredentials:
- json['useCustomSpotifyCredentials'] as bool? ?? true,
+ json['useCustomSpotifyCredentials'] as bool? ?? false,
metadataSource: json['metadataSource'] as String? ?? 'deezer',
enableLogging: json['enableLogging'] as bool? ?? false,
useExtensionProviders: json['useExtensionProviders'] as bool? ?? true,
@@ -55,6 +55,7 @@ AppSettings _$AppSettingsFromJson(Map json) => AppSettings(
songLinkRegion: json['songLinkRegion'] as String? ?? 'US',
localLibraryEnabled: json['localLibraryEnabled'] as bool? ?? false,
localLibraryPath: json['localLibraryPath'] as String? ?? '',
+ localLibraryBookmark: json['localLibraryBookmark'] as String? ?? '',
localLibraryShowDuplicates:
json['localLibraryShowDuplicates'] as bool? ?? true,
hasCompletedTutorial: json['hasCompletedTutorial'] as bool? ?? false,
@@ -128,6 +129,7 @@ Map _$AppSettingsToJson(
'songLinkRegion': instance.songLinkRegion,
'localLibraryEnabled': instance.localLibraryEnabled,
'localLibraryPath': instance.localLibraryPath,
+ 'localLibraryBookmark': instance.localLibraryBookmark,
'localLibraryShowDuplicates': instance.localLibraryShowDuplicates,
'hasCompletedTutorial': instance.hasCompletedTutorial,
'lyricsProviders': instance.lyricsProviders,
diff --git a/lib/models/track.dart b/lib/models/track.dart
index 244a7a65..e4d850c2 100644
--- a/lib/models/track.dart
+++ b/lib/models/track.dart
@@ -21,6 +21,7 @@ class Track {
final ServiceAvailability? availability;
final String? source;
final String? albumType;
+ final int? totalTracks;
final String? itemType;
const Track({
@@ -41,10 +42,21 @@ class Track {
this.availability,
this.source,
this.albumType,
+ this.totalTracks,
this.itemType,
});
- bool get isSingle => albumType == 'single' || albumType == 'ep';
+ bool get isSingle {
+ switch (albumType?.toLowerCase()) {
+ case 'single':
+ return true;
+ case 'ep':
+ final count = totalTracks;
+ return count == null || count <= 1;
+ default:
+ return false;
+ }
+ }
bool get isAlbumItem => itemType == 'album';
diff --git a/lib/models/track.g.dart b/lib/models/track.g.dart
index f640cfe7..5e361ab6 100644
--- a/lib/models/track.g.dart
+++ b/lib/models/track.g.dart
@@ -28,6 +28,7 @@ Track _$TrackFromJson(Map json) => Track(
),
source: json['source'] as String?,
albumType: json['albumType'] as String?,
+ totalTracks: (json['totalTracks'] as num?)?.toInt(),
itemType: json['itemType'] as String?,
);
@@ -49,6 +50,7 @@ Map _$TrackToJson(Track instance) => {
'availability': instance.availability,
'source': instance.source,
'albumType': instance.albumType,
+ 'totalTracks': instance.totalTracks,
'itemType': instance.itemType,
};
diff --git a/lib/providers/download_queue_provider.dart b/lib/providers/download_queue_provider.dart
index 4f2059db..18348f96 100644
--- a/lib/providers/download_queue_provider.dart
+++ b/lib/providers/download_queue_provider.dart
@@ -251,9 +251,11 @@ class DownloadHistoryState {
class DownloadHistoryNotifier extends Notifier {
static const int _safRepairBatchSize = 20;
static const int _safRepairMaxPerLaunch = 60;
+ static const int _audioMetadataBackfillMaxPerLaunch = 24;
final HistoryDatabase _db = HistoryDatabase.instance;
bool _isLoaded = false;
bool _isSafRepairInProgress = false;
+ bool _isAudioMetadataBackfillInProgress = false;
@override
DownloadHistoryState build() {
@@ -298,9 +300,19 @@ class DownloadHistoryNotifier extends Notifier {
maxItems: _safRepairMaxPerLaunch,
);
await cleanupOrphanedDownloads();
+ await _backfillAudioMetadata(
+ state.items,
+ maxItems: _audioMetadataBackfillMaxPerLaunch,
+ );
});
} else {
- Future.microtask(() => cleanupOrphanedDownloads());
+ Future.microtask(() async {
+ await cleanupOrphanedDownloads();
+ await _backfillAudioMetadata(
+ state.items,
+ maxItems: _audioMetadataBackfillMaxPerLaunch,
+ );
+ });
}
} catch (e, stack) {
_historyLog.e('Failed to load history from database: $e', e, stack);
@@ -429,6 +441,157 @@ class DownloadHistoryNotifier extends Notifier {
}
}
+ int? _readPositiveInt(dynamic value) {
+ if (value == null) return null;
+ if (value is num) {
+ final asInt = value.toInt();
+ return asInt > 0 ? asInt : null;
+ }
+ final parsed = int.tryParse(value.toString());
+ if (parsed == null || parsed <= 0) return null;
+ return parsed;
+ }
+
+ bool _supportsAudioMetadataProbe(String filePath) {
+ final trimmed = filePath.trim().toLowerCase();
+ if (trimmed.isEmpty) return false;
+ if (trimmed.startsWith('content://')) return true;
+ return trimmed.endsWith('.flac') ||
+ trimmed.endsWith('.m4a') ||
+ trimmed.endsWith('.aac') ||
+ trimmed.endsWith('.mp3') ||
+ trimmed.endsWith('.opus') ||
+ trimmed.endsWith('.ogg');
+ }
+
+ bool _shouldBackfillAudioMetadata(DownloadHistoryItem item) {
+ if (!_supportsAudioMetadataProbe(item.filePath)) {
+ return false;
+ }
+
+ final trimmedPath = item.filePath.trim().toLowerCase();
+ final hasResolvedSpecs =
+ item.bitDepth != null &&
+ item.bitDepth! > 0 &&
+ item.sampleRate != null &&
+ item.sampleRate! > 0;
+ final needsLosslessSpecProbe =
+ !hasResolvedSpecs &&
+ (trimmedPath.endsWith('.flac') ||
+ trimmedPath.endsWith('.m4a') ||
+ trimmedPath.endsWith('.aac') ||
+ trimmedPath.startsWith('content://'));
+
+ if (hasResolvedSpecs && !isPlaceholderQualityLabel(item.quality)) {
+ return false;
+ }
+
+ return needsLosslessSpecProbe ||
+ isPlaceholderQualityLabel(item.quality) ||
+ normalizeOptionalString(item.quality) == null;
+ }
+
+ Future