diff --git a/android/app/src/main/kotlin/com/zarz/spotiflac/DownloadService.kt b/android/app/src/main/kotlin/com/zarz/spotiflac/DownloadService.kt index fe9b8e07..f05542bc 100644 --- a/android/app/src/main/kotlin/com/zarz/spotiflac/DownloadService.kt +++ b/android/app/src/main/kotlin/com/zarz/spotiflac/DownloadService.kt @@ -137,14 +137,13 @@ class DownloadService : Service() { private fun startForegroundService() { isRunning = true - - // Acquire wake lock to prevent CPU sleep + val powerManager = getSystemService(Context.POWER_SERVICE) as PowerManager wakeLock = powerManager.newWakeLock( PowerManager.PARTIAL_WAKE_LOCK, WAKELOCK_TAG ).apply { - acquire(60 * 60 * 1000L) // 1 hour max + acquire(60 * 60 * 1000L) } val notification = buildNotification(0, 0) 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 e282cc52..dbdd74ff 100644 --- a/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt +++ b/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt @@ -130,39 +130,35 @@ class MainActivity: FlutterFragmentActivity() { ) companion object { - // Minimum API level we consider "safe" for Impeller (Android 10+) private const val SAFE_API_FOR_IMPELLER = 29 - - // Known problematic GPU patterns (lowercase) + private val PROBLEMATIC_GPU_PATTERNS = listOf( - "adreno (tm) 3", // Adreno 300 series (305, 320, 330, etc.) - old Qualcomm - "adreno (tm) 4", // Adreno 400 series - some have issues - "mali-4", // Mali-400 series - old ARM GPUs - "mali-t6", // Mali-T600 series - "mali-t7", // Mali-T700 series (some) - "powervr sgx", // PowerVR SGX series - old Imagination GPUs - "powervr ge8320", // PowerVR GE8320 - known issues - "gc1000", // Vivante GC1000 - "gc2000", // Vivante GC2000 + "adreno (tm) 3", + "adreno (tm) 4", + "mali-4", + "mali-t6", + "mali-t7", + "powervr sgx", + "powervr ge8320", + "gc1000", + "gc2000", ) - - // Known problematic chipsets/hardware (lowercase) + private val PROBLEMATIC_CHIPSETS = listOf( - "mt6762", // MediaTek Helio P22 with PowerVR GE8320 - "mt6765", // MediaTek Helio P35 with PowerVR GE8320 - "mt8768", // MediaTek tablet chip - "mp0873", // MediaTek variant - "msm8974", // Snapdragon 800/801 with Adreno 330 - "msm8226", // Snapdragon 400 with Adreno 305 - "msm8926", // Snapdragon 400 with Adreno 305 - "apq8084", // Snapdragon 805 (some issues) + "mt6762", + "mt6765", + "mt8768", + "mp0873", + "msm8974", + "msm8226", + "msm8926", + "apq8084", ) - - // Known problematic device models (lowercase) + private val PROBLEMATIC_MODELS = listOf( - "sm-t220", // Samsung Tab A7 Lite - "sm-t225", // Samsung Tab A7 Lite LTE - "hammerhead", // Nexus 5 (Adreno 330) + "sm-t220", + "sm-t225", + "hammerhead", ) /** * Check if device should use Skia instead of Impeller. @@ -174,7 +170,6 @@ class MainActivity: FlutterFragmentActivity() { val model = Build.MODEL.lowercase(Locale.ROOT) val device = Build.DEVICE.lowercase(Locale.ROOT) - // 1. Check for explicitly problematic device models for (problematicModel in PROBLEMATIC_MODELS) { if (model.contains(problematicModel) || device.contains(problematicModel)) { android.util.Log.i("SpotiFLAC", "Matched problematic model: $problematicModel") @@ -182,7 +177,6 @@ class MainActivity: FlutterFragmentActivity() { } } - // 2. Check for problematic chipsets for (chipset in PROBLEMATIC_CHIPSETS) { if (hardware.contains(chipset) || board.contains(chipset)) { android.util.Log.i("SpotiFLAC", "Matched problematic chipset: $chipset") @@ -190,12 +184,9 @@ class MainActivity: FlutterFragmentActivity() { } } - // 3. For Android < 10 (API 29), be more aggressive about disabling Impeller if (Build.VERSION.SDK_INT < SAFE_API_FOR_IMPELLER) { - // For older Android, check GPU renderer if available val gpuRenderer = getGpuRenderer().lowercase(Locale.ROOT) - // Check for known problematic GPUs for (pattern in PROBLEMATIC_GPU_PATTERNS) { if (gpuRenderer.contains(pattern)) { android.util.Log.i("SpotiFLAC", "Matched problematic GPU on old Android: $pattern") @@ -203,14 +194,12 @@ class MainActivity: FlutterFragmentActivity() { } } - // For very old Android (< 8.0), always use Skia as Vulkan support is spotty if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { android.util.Log.i("SpotiFLAC", "Android < 8.0, using Skia for safety") return true } } - // 4. For Android 10+, still check for known problematic GPUs val gpuRenderer = getGpuRenderer().lowercase(Locale.ROOT) for (pattern in PROBLEMATIC_GPU_PATTERNS) { if (gpuRenderer.contains(pattern)) { @@ -228,8 +217,6 @@ class MainActivity: FlutterFragmentActivity() { */ private fun getGpuRenderer(): String { return try { - // This might not work before GL context is created, - // but worth trying for additional detection android.opengl.GLES20.glGetString(android.opengl.GLES20.GL_RENDERER) ?: "" } catch (e: Exception) { "" @@ -632,7 +619,6 @@ class MainActivity: FlutterFragmentActivity() { * Resolve extension from a MediaStore URI by querying DISPLAY_NAME or MIME_TYPE. */ private fun resolveMediaStoreExt(uri: Uri, fallbackExt: String?): String { - // Try DISPLAY_NAME first try { contentResolver.query(uri, arrayOf(android.provider.MediaStore.MediaColumns.DISPLAY_NAME), null, null, null)?.use { cursor -> if (cursor.moveToFirst()) { @@ -643,7 +629,6 @@ class MainActivity: FlutterFragmentActivity() { } } catch (_: Exception) {} - // Try MIME_TYPE try { val mime = contentResolver.getType(uri) val ext = extFromMimeType(mime) @@ -869,8 +854,6 @@ class MainActivity: FlutterFragmentActivity() { val mimeType = mimeTypeForExt(outputExt) val fileName = buildSafFileName(req, outputExt) - // Check for existing file WITHOUT creating the directory first. - // This prevents empty folders from being created for duplicate downloads. val existingDir = findDocumentDir(treeUri, relativeDir) if (existingDir != null) { val existing = existingDir.findFile(fileName) @@ -885,7 +868,6 @@ class MainActivity: FlutterFragmentActivity() { } } - // Only create the directory now that we know we need to download val targetDir = ensureDocumentDir(treeUri, relativeDir) ?: return errorJson("Failed to access SAF directory") @@ -908,7 +890,6 @@ class MainActivity: FlutterFragmentActivity() { 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://") && @@ -957,15 +938,10 @@ class MainActivity: FlutterFragmentActivity() { 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 @@ -990,21 +966,17 @@ class MainActivity: FlutterFragmentActivity() { 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("\\") } } @@ -1089,7 +1061,6 @@ class MainActivity: FlutterFragmentActivity() { 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() val safChildLookupCache = mutableMapOf>() @@ -1174,7 +1145,6 @@ 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) { @@ -1213,10 +1183,8 @@ class MainActivity: FlutterFragmentActivity() { 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) @@ -1230,7 +1198,6 @@ class MainActivity: FlutterFragmentActivity() { 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) { @@ -1273,14 +1240,12 @@ class MainActivity: FlutterFragmentActivity() { } } - // --- 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 @@ -1359,7 +1324,6 @@ class MainActivity: FlutterFragmentActivity() { return result.toString() } - // Parse existing files map: URI -> lastModified val existingFiles = mutableMapOf() try { val obj = JSONObject(existingFilesJson) @@ -1378,20 +1342,15 @@ class MainActivity: FlutterFragmentActivity() { } val supportedAudioExt = setOf(".flac", ".m4a", ".mp3", ".opus", ".ogg") - val audioFiles = mutableListOf>() // doc, path, lastModified - // CUE files to scan: (cueDoc, parentDir, lastModified) + val audioFiles = mutableListOf>() 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() val safChildLookupCache = mutableMapOf>() var traversalErrors = 0 - // 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] + val existingCueVirtualPaths = mutableMapOf>() for (key in existingFiles.keys) { val hashIdx = key.indexOf("#track") if (hashIdx > 0) { @@ -1400,7 +1359,6 @@ class MainActivity: FlutterFragmentActivity() { } } - // Collect all files with lastModified val queue: ArrayDeque> = ArrayDeque() queue.add(root to "") @@ -1456,8 +1414,6 @@ class MainActivity: FlutterFragmentActivity() { } queue.add(child to childPath) } else if (child.isFile) { - // Mark file as present first so it cannot be mis-classified as removed - // when provider-specific metadata calls (e.g., lastModified) fail. val uriStr = child.uri.toString() currentUris.add(uriStr) @@ -1469,18 +1425,15 @@ class MainActivity: FlutterFragmentActivity() { 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")) { @@ -1491,7 +1444,6 @@ class MainActivity: FlutterFragmentActivity() { existingModified ?: 0L } - // Check if file is new or modified if (existingModified == null || existingModified != lastModified) { audioFiles.add(Triple(child, path, lastModified)) } @@ -1508,7 +1460,6 @@ 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 filesToProcess = audioFiles.size + cueFilesToScan.size @@ -1536,7 +1487,6 @@ 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) { @@ -1557,7 +1507,6 @@ class MainActivity: FlutterFragmentActivity() { var tempCuePath: String? = null var tempAudioPath: String? = null try { - // Copy CUE to temp tempCuePath = copyUriToTemp(cueDoc.uri, ".cue") if (tempCuePath == null) { errors++ @@ -1566,10 +1515,8 @@ class MainActivity: FlutterFragmentActivity() { 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 val audioDoc = resolveCueAudioSibling( parentDir = parentDir, cueName = cueName, @@ -1584,10 +1531,8 @@ class MainActivity: FlutterFragmentActivity() { 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) @@ -1601,7 +1546,6 @@ class MainActivity: FlutterFragmentActivity() { 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) { @@ -1609,7 +1553,6 @@ class MainActivity: FlutterFragmentActivity() { tempAudioPath = renamedAudio.absolutePath } - // Call Go to produce library scan entries for each CUE track val cueResultsJson = Gobackend.scanCueSheetForLibrary( tempCuePath, tempDir, @@ -1621,7 +1564,6 @@ class MainActivity: FlutterFragmentActivity() { 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) @@ -1654,9 +1596,6 @@ class MainActivity: FlutterFragmentActivity() { } } - // 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 { @@ -1681,7 +1620,6 @@ class MainActivity: FlutterFragmentActivity() { } } - // --- Regular audio file pass: skip files referenced by CUE sheets --- for ((doc, _, lastModified) in audioFiles) { if (safScanCancel) { updateSafScanProgress { it.isComplete = true } @@ -1694,7 +1632,6 @@ 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 @@ -1748,7 +1685,6 @@ class MainActivity: FlutterFragmentActivity() { } } - // Recalculate removedUris now that CUE virtual paths have been registered val finalRemovedUris = existingFiles.keys.filter { !currentUris.contains(it) } updateSafScanProgress { @@ -1926,7 +1862,6 @@ class MainActivity: FlutterFragmentActivity() { override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) - // Update the intent so receive_sharing_intent can access the new data setIntent(intent) } @@ -2586,7 +2521,6 @@ class MainActivity: FlutterFragmentActivity() { val tempPath = copyUriToTemp(uri) ?: return@withContext """{"error":"Failed to copy SAF file to temp"}""" try { - // Replace file_path with temp path for Go reqObj.put("file_path", tempPath) val raw = Gobackend.reEnrichFile(reqObj.toString()) val obj = JSONObject(raw) @@ -2664,7 +2598,6 @@ class MainActivity: FlutterFragmentActivity() { } result.success(null) } - // Deezer API methods "searchDeezerAll" -> { val query = call.argument("query") ?: "" val trackLimit = call.argument("track_limit") ?: 15 @@ -2675,7 +2608,6 @@ class MainActivity: FlutterFragmentActivity() { } result.success(response) } - // Tidal search API "searchTidalAll" -> { val query = call.argument("query") ?: "" val trackLimit = call.argument("track_limit") ?: 15 @@ -2686,7 +2618,6 @@ class MainActivity: FlutterFragmentActivity() { } result.success(response) } - // Qobuz search API "searchQobuzAll" -> { val query = call.argument("query") ?: "" val trackLimit = call.argument("track_limit") ?: 15 @@ -2816,7 +2747,6 @@ class MainActivity: FlutterFragmentActivity() { } result.success(response) } - // Log methods "getLogs" -> { val response = withContext(Dispatchers.IO) { Gobackend.getLogs() @@ -2849,7 +2779,6 @@ class MainActivity: FlutterFragmentActivity() { } result.success(null) } - // Extension System methods "initExtensionSystem" -> { val extensionsDir = call.argument("extensions_dir") ?: "" val dataDir = call.argument("data_dir") ?: "" @@ -2994,7 +2923,6 @@ class MainActivity: FlutterFragmentActivity() { } result.success(null) } - // Extension Auth API methods "getExtensionPendingAuth" -> { val extensionId = call.argument("extension_id") ?: "" val response = withContext(Dispatchers.IO) { @@ -3044,7 +2972,6 @@ class MainActivity: FlutterFragmentActivity() { } result.success(response) } - // Extension FFmpeg API "getPendingFFmpegCommand" -> { val commandId = call.argument("command_id") ?: "" val response = withContext(Dispatchers.IO) { @@ -3072,7 +2999,6 @@ class MainActivity: FlutterFragmentActivity() { } result.success(response) } - // Extension Custom Search API "customSearchWithExtension" -> { val extensionId = call.argument("extension_id") ?: "" val query = call.argument("query") ?: "" @@ -3088,7 +3014,6 @@ class MainActivity: FlutterFragmentActivity() { } result.success(response) } - // Extension URL Handler API "handleURLWithExtension" -> { val url = call.argument("url") ?: "" val response = withContext(Dispatchers.IO) { @@ -3133,7 +3058,6 @@ class MainActivity: FlutterFragmentActivity() { } result.success(response) } - // Extension Post-Processing API "runPostProcessing" -> { val filePath = call.argument("file_path") ?: "" val metadataJson = call.argument("metadata") ?: "" @@ -3177,7 +3101,6 @@ class MainActivity: FlutterFragmentActivity() { } result.success(response) } - // Extension Store "initExtensionStore" -> { val cacheDir = call.argument("cache_dir") ?: "" withContext(Dispatchers.IO) { @@ -3239,7 +3162,6 @@ class MainActivity: FlutterFragmentActivity() { } result.success(null) } - // Extension Home Feed (Explore) "getExtensionHomeFeed" -> { val extensionId = call.argument("extension_id") ?: "" val response = withContext(Dispatchers.IO) { @@ -3254,7 +3176,6 @@ class MainActivity: FlutterFragmentActivity() { } result.success(response) } - // Local Library Scanning "setLibraryCoverCacheDir" -> { val cacheDir = call.argument("cache_dir") ?: "" withContext(Dispatchers.IO) { @@ -3359,7 +3280,6 @@ class MainActivity: FlutterFragmentActivity() { } result.success(response) } - // CUE Sheet Parsing "parseCueSheet" -> { val cuePath = call.argument("cue_path") ?: "" val audioDir = call.argument("audio_dir") ?: "" @@ -3371,17 +3291,14 @@ class MainActivity: FlutterFragmentActivity() { ?: 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 ?: "" @@ -3400,7 +3317,6 @@ class MainActivity: FlutterFragmentActivity() { 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 @@ -3415,15 +3331,11 @@ class MainActivity: FlutterFragmentActivity() { } } - // 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 { diff --git a/go_backend/cover.go b/go_backend/cover.go index 02d1f03f..a368fb8f 100644 --- a/go_backend/cover.go +++ b/go_backend/cover.go @@ -42,7 +42,6 @@ func downloadCoverToMemory(coverURL string, maxQuality bool) ([]byte, error) { maxURL := upgradeToMaxQuality(downloadURL) if maxURL != downloadURL { downloadURL = maxURL - // Log already printed by upgradeToMaxQuality for Deezer if strings.Contains(coverURL, "scdn.co") || strings.Contains(coverURL, "spotifycdn") { GoLog("[Cover] Spotify: upgraded to max resolution (~2000x2000)") } @@ -88,22 +87,18 @@ func downloadCoverToMemory(coverURL string, maxQuality bool) ([]byte, error) { } func upgradeToMaxQuality(coverURL string) string { - // Spotify CDN upgrade if strings.Contains(coverURL, spotifySize640) { return strings.Replace(coverURL, spotifySize640, spotifySizeMax, 1) } - // Deezer CDN upgrade if strings.Contains(coverURL, "cdn-images.dzcdn.net") { return upgradeDeezerCover(coverURL) } - // Tidal CDN upgrade: 1280x1280 → origin if strings.Contains(coverURL, "resources.tidal.com") { return upgradeTidalCover(coverURL) } - // Qobuz CDN upgrade: _600 → _max if strings.Contains(coverURL, "static.qobuz.com") { return upgradeQobuzCover(coverURL) } @@ -152,7 +147,6 @@ func GetCoverFromSpotify(imageURL string, maxQuality bool) string { return "" } - // Always upgrade small to medium first result := convertSmallToMedium(imageURL) if maxQuality { diff --git a/go_backend/cue_parser.go b/go_backend/cue_parser.go index 8cbf1f61..1a0dfa06 100644 --- a/go_backend/cue_parser.go +++ b/go_backend/cue_parser.go @@ -13,7 +13,6 @@ import ( // 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"` @@ -32,7 +31,6 @@ type CueTrack struct { 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) } @@ -82,7 +80,6 @@ func ParseCueFile(cuePath string) (*CueSheet, error) { 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) @@ -90,7 +87,6 @@ func ParseCueFile(cuePath string) (*CueSheet, error) { upper := strings.ToUpper(line) - // REM commands (album-level metadata) if strings.HasPrefix(upper, "REM ") { matches := reRemCommand.FindStringSubmatch(line) if len(matches) == 3 { @@ -136,9 +132,6 @@ func ParseCueFile(cuePath string) (*CueSheet, error) { 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 @@ -146,7 +139,6 @@ func ParseCueFile(cuePath string) (*CueSheet, error) { } if strings.HasPrefix(upper, "TRACK ") { - // Save previous track if currentTrack != nil { sheet.Tracks = append(sheet.Tracks, *currentTrack) } @@ -184,7 +176,6 @@ func ParseCueFile(cuePath string) (*CueSheet, error) { continue } - // SONGWRITER (used as composer sometimes) if strings.HasPrefix(upper, "SONGWRITER ") { value := unquoteCue(line[len("SONGWRITER "):]) if currentTrack != nil { @@ -196,7 +187,6 @@ func ParseCueFile(cuePath string) (*CueSheet, error) { } } - // Don't forget the last track if currentTrack != nil { sheet.Tracks = append(sheet.Tracks, *currentTrack) } diff --git a/go_backend/deezer_download.go b/go_backend/deezer_download.go index 974ffcf6..fdd0850d 100644 --- a/go_backend/deezer_download.go +++ b/go_backend/deezer_download.go @@ -319,7 +319,6 @@ func (c *DeezerClient) GetMusicDLDownloadURL(deezerTrackURL string) (string, err return "", fmt.Errorf("MusicDL error: %s", errMsg) } - // Try various response fields for download URL for _, key := range []string{"download_url", "url", "link"} { if urlVal, ok := raw[key].(string); ok && strings.TrimSpace(urlVal) != "" { return strings.TrimSpace(urlVal), nil diff --git a/go_backend/exports.go b/go_backend/exports.go index 830f11bb..f104e763 100644 --- a/go_backend/exports.go +++ b/go_backend/exports.go @@ -1906,7 +1906,6 @@ func ReEnrichFile(requestJSON string) (string, error) { } } - // Log metadata summary before embedding GoLog("[ReEnrich] Metadata to embed: title=%s, artist=%s, album=%s, albumArtist=%s\n", req.TrackName, req.ArtistName, req.AlbumName, req.AlbumArtist) GoLog("[ReEnrich] track=%d, disc=%d, date=%s, isrc=%s, genre=%s, label=%s\n", @@ -1989,7 +1988,6 @@ func ReEnrichFile(requestJSON string) (string, error) { } } - // Build enriched metadata response for Dart (includes online search results) enrichedMeta := map[string]interface{}{ "track_name": req.TrackName, "artist_name": req.ArtistName, diff --git a/go_backend/extension_store.go b/go_backend/extension_store.go index ed8915ab..6378139a 100644 --- a/go_backend/extension_store.go +++ b/go_backend/extension_store.go @@ -236,7 +236,6 @@ func (s *ExtensionStore) FetchRegistry(forceRefresh bool) (*StoreRegistry, error s.cacheMu.Lock() defer s.cacheMu.Unlock() - // Check if a registry URL has been configured if s.registryURL == "" { return nil, fmt.Errorf("no registry URL configured. Please add a repository URL first") } @@ -396,7 +395,6 @@ func ResolveRegistryURL(input string) (string, error) { return input, nil } - // Try to match https://github.com//[/...] const ghPrefix = "https://github.com/" if !strings.HasPrefix(input, ghPrefix) { // Also accept http:// and upgrade silently. @@ -500,7 +498,6 @@ func (s *ExtensionStore) SearchExtensions(query string, category string) ([]*Sto !containsIgnoreCase(ext.DisplayName, queryLower) && !containsIgnoreCase(ext.Description, queryLower) && !containsIgnoreCase(ext.Author, queryLower) { - // Check tags found := false for _, tag := range ext.Tags { if containsIgnoreCase(tag, queryLower) { diff --git a/go_backend/httputil_utls.go b/go_backend/httputil_utls.go index 4b09deb7..c3a90670 100644 --- a/go_backend/httputil_utls.go +++ b/go_backend/httputil_utls.go @@ -112,7 +112,6 @@ func DoRequestWithCloudflareBypass(req *http.Request) (*http.Response, error) { resp, err := sharedClient.Do(req) if err == nil { - // Check for Cloudflare challenge page (403 with specific markers) if resp.StatusCode == 403 || resp.StatusCode == 503 { body, readErr := io.ReadAll(resp.Body) resp.Body.Close() @@ -154,7 +153,6 @@ func DoRequestWithCloudflareBypass(req *http.Request) (*http.Response, error) { return resp, nil } - // Check if error might be TLS-related (Cloudflare blocking) errStr := strings.ToLower(err.Error()) tlsRelated := strings.Contains(errStr, "tls") || strings.Contains(errStr, "handshake") || diff --git a/go_backend/library_scan.go b/go_backend/library_scan.go index ee8874d1..c56b57d9 100644 --- a/go_backend/library_scan.go +++ b/go_backend/library_scan.go @@ -234,8 +234,6 @@ func ScanLibraryFolder(folderPath string) (string, error) { 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 @@ -557,9 +555,6 @@ func ReadAudioMetadataWithDisplayName(filePath, displayNameHint string) (string, return string(jsonBytes), nil } -// ScanLibraryFolderIncremental performs an incremental scan of the library folder -// existingFilesJSON is a JSON object mapping filePath -> modTime (unix millis) -// Only files that are new or have changed modification time will be scanned func loadExistingFilesSnapshot(snapshotPath string) (map[string]int64, error) { existingFiles := make(map[string]int64) if snapshotPath == "" { @@ -637,7 +632,6 @@ func scanLibraryFolderIncrementalWithExistingFiles(folderPath string, existingFi libraryScanProgress.TotalFiles = totalFiles libraryScanProgressMu.Unlock() - // Find files to scan (new or modified) var filesToScan []libraryAudioFileInfo skippedCount := 0 existingCueTrackModTimes := make(map[string]int64) @@ -653,10 +647,8 @@ func scanLibraryFolderIncrementalWithExistingFiles(folderPath string, existingFi for _, f := range currentFiles { existingModTime, exists := existingFiles[f.path] if !exists { - // For .cue files, also check if any virtual path entries exist if strings.ToLower(filepath.Ext(f.path)) == ".cue" { if cueTrackModTime, hasCueTracks := existingCueTrackModTimes[f.path]; hasCueTracks { - // CUE file exists in DB via virtual paths; check if modTime changed if f.modTime == cueTrackModTime { skippedCount++ } else { @@ -675,14 +667,11 @@ func scanLibraryFolderIncrementalWithExistingFiles(folderPath string, existingFi var deletedPaths []string for existingPath := range existingFiles { - // 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 + continue } - // Base CUE file is gone, mark virtual path as deleted deletedPaths = append(deletedPaths, existingPath) } else if !currentPathSet[existingPath] { deletedPaths = append(deletedPaths, existingPath) @@ -713,7 +702,6 @@ func scanLibraryFolderIncrementalWithExistingFiles(folderPath string, existingFi 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) parsedCueFiles := make(map[string]scannedCueFileInfo) for _, f := range filesToScan { @@ -748,7 +736,6 @@ func scanLibraryFolderIncrementalWithExistingFiles(folderPath string, existingFi ext := strings.ToLower(filepath.Ext(f.path)) - // Handle .cue files: produce multiple track results if ext == ".cue" { var cueResults []LibraryScanResult cueInfo, ok := parsedCueFiles[f.path] @@ -773,7 +760,6 @@ func scanLibraryFolderIncrementalWithExistingFiles(folderPath string, existingFi continue } - // Skip audio files referenced by .cue sheets if cueReferencedAudioFilesInc[f.path] { continue } diff --git a/go_backend/title_match_utils.go b/go_backend/title_match_utils.go index cecf462d..b5089065 100644 --- a/go_backend/title_match_utils.go +++ b/go_backend/title_match_utils.go @@ -24,11 +24,9 @@ func normalizeLooseTitle(title string) string { b.WriteRune(r) case unicode.IsSpace(r): b.WriteByte(' ') - // Treat common separators as spaces. case r == '/', r == '\\', r == '_', r == '-', r == '|', r == '.', r == '&', r == '+': b.WriteByte(' ') default: - // Drop other punctuation/symbols (including emoji) for loose matching. } } @@ -59,7 +57,6 @@ func normalizeLooseArtistName(name string) string { case r == '/', r == '\\', r == '_', r == '-', r == '|', r == '.', r == '&', r == '+': b.WriteByte(' ') default: - // Drop remaining punctuation/symbols for loose artist matching. } } @@ -102,13 +99,11 @@ func normalizeSymbolOnlyTitle(title string) string { return b.String() } -// ==================== Shared Track Verification ==================== - // resolvedTrackInfo holds the metadata fetched from a provider for verification. type resolvedTrackInfo struct { Title string ArtistName string - Duration int // seconds + Duration int } // trackMatchesRequest checks whether a resolved track from a provider matches diff --git a/lib/providers/download_queue_provider.dart b/lib/providers/download_queue_provider.dart index 57f0e1f3..14693e35 100644 --- a/lib/providers/download_queue_provider.dart +++ b/lib/providers/download_queue_provider.dart @@ -874,10 +874,6 @@ class DownloadHistoryNotifier extends Notifier { await _db.upsert(updated.toJson()); } - /// Remove history entries where the file no longer exists on disk. - /// Returns the number of orphaned entries removed. - - /// Audio file extensions that the app commonly produces or converts between. static const _audioExtensions = [ '.flac', '.m4a', @@ -888,9 +884,6 @@ class DownloadHistoryNotifier extends Notifier { '.aac', ]; - /// When the original file is missing, check whether a sibling with a - /// different audio extension exists (e.g. the user converted .flac → .opus). - /// Returns the path of the first match found, or `null` if none exist. Future _findConvertedSibling(String originalPath) async { final dotIndex = originalPath.lastIndexOf('.'); if (dotIndex < 0) return null; @@ -2711,7 +2704,6 @@ class DownloadQueueNotifier extends Notifier { static final _deezerSizeRegex = RegExp(r'/(\d+)x(\d+)-\d+-\d+-\d+-\d+\.jpg$'); String _upgradeToMaxQualityCover(String coverUrl) { - // Spotify CDN upgrade (hash-based size identifiers) const spotifySize300 = 'ab67616d00001e02'; const spotifySize640 = 'ab67616d0000b273'; const spotifySizeMax = 'ab67616d000082c1'; @@ -2724,7 +2716,6 @@ class DownloadQueueNotifier extends Notifier { result = result.replaceFirst(spotifySize640, spotifySizeMax); } - // Deezer CDN upgrade (1000x1000 → 1800x1800) if (result.contains('cdn-images.dzcdn.net')) { final upgraded = result.replaceFirst( _deezerSizeRegex, @@ -3405,7 +3396,6 @@ class DownloadQueueNotifier extends Notifier { Future _processQueue() async { if (state.isProcessing) return; - // Check network connectivity before starting final settings = ref.read(settingsProvider); updateSettings(settings); final isSafMode = _isSafMode(settings); @@ -3465,7 +3455,6 @@ class DownloadQueueNotifier extends Notifier { state = state.copyWith(outputDir: musicDir.path); ref.read(settingsProvider.notifier).setDownloadDirectory(musicDir.path); } else if (!isValidIosWritablePath(state.outputDir)) { - // Check for other invalid paths (like container root without Documents/) _log.w( 'iOS: Invalid output path detected (container root?), falling back to app Documents folder', ); @@ -3487,7 +3476,6 @@ class DownloadQueueNotifier extends Notifier { _log.d('Output directory: ${state.outputDir}'); } else { _log.d('Output directory: SAF (tree_uri=${settings.downloadTreeUri})'); - // Validate SAF permission is still accessible try { final testResult = await PlatformBridge.createSafFileFromPath( treeUri: settings.downloadTreeUri, @@ -3496,16 +3484,12 @@ class DownloadQueueNotifier extends Notifier { mimeType: 'application/octet-stream', srcPath: '', ); - // If we got a result, permission is valid (file creation may fail but that's ok) - // If permission is revoked, this will throw if (testResult != null) { - // Clean up test file await PlatformBridge.safDelete(testResult); } } catch (e) { _log.e('SAF permission validation failed: $e'); _log.w('SAF tree URI may be invalid or permission revoked'); - // Mark all queued items as failed for (final item in state.items) { if (item.status == DownloadStatus.queued) { updateItemStatus( @@ -3639,8 +3623,6 @@ class DownloadQueueNotifier extends Notifier { } if (activeDownloads.isNotEmpty) { - // Re-check queue/settings periodically so concurrency changes - // (e.g. 1 -> 3) can take effect before any active item finishes. await Future.any([ Future.any(activeDownloads.values), Future.delayed(_queueSchedulingInterval), @@ -3926,7 +3908,6 @@ class DownloadQueueNotifier extends Notifier { if (resolvedIsrc != null && _isValidISRC(resolvedIsrc)) { _log.d('Resolved ISRC from $provider: $resolvedIsrc'); - // Enrich track with provider metadata final provReleaseDate = normalizeOptionalString( trackData['release_date'] as String?, ); @@ -3962,7 +3943,6 @@ class DownloadQueueNotifier extends Notifier { source: trackToDownload.source, ); - // Search Deezer by the resolved ISRC try { final deezerResult = await PlatformBridge.searchDeezerByISRC( resolvedIsrc, @@ -3988,9 +3968,6 @@ class DownloadQueueNotifier extends Notifier { } } - // Fallback: Use SongLink to convert Spotify ID to Deezer ID - // Skip for tidal:/qobuz: IDs – they are not Spotify URLs and the - // provider ISRC resolution above already handles them. if (!selectedExtensionDownloadProvider && deezerTrackId == null && !shouldSkipExtensionSongLinkPrelookup && @@ -4011,7 +3988,6 @@ class DownloadQueueNotifier extends Notifier { 'track', spotifyId, ); - // Response is TrackResponse: {"track": {"spotify_id": "deezer:XXXXX", ...}} final trackData = deezerData['track']; if (trackData is Map) { final rawId = trackData['spotify_id'] as String?; @@ -4317,7 +4293,6 @@ class DownloadQueueNotifier extends Notifier { finalSafFileName = reportedFileName; } - // Check if file already existed (detected via ISRC match in Go backend) final wasExisting = result['already_exists'] == true; if (wasExisting) { _log.i('File already exists in library: $filePath'); @@ -4330,7 +4305,6 @@ class DownloadQueueNotifier extends Notifier { String actualQuality = quality; if (actualBitDepth != null && actualBitDepth > 0) { - // Format: "24-bit/96kHz" or "16-bit/44.1kHz" final sampleRateKHz = actualSampleRate != null && actualSampleRate > 0 ? (actualSampleRate / 1000).toStringAsFixed( actualSampleRate % 1000 == 0 ? 0 : 1, @@ -4486,7 +4460,6 @@ class DownloadQueueNotifier extends Notifier { } if (isM4aFile || shouldForceTidalSafM4aHandling) { - // At this point filePath is guaranteed non-null by the checks above. final currentFilePath = filePath; if (isContentUriPath && effectiveSafMode) { @@ -4979,9 +4952,6 @@ class DownloadQueueNotifier extends Notifier { return; } - // SAF downloads should end with content URI. If we still have a - // transient FD path, recover URI from SAF metadata to keep history - // dedup/exclusion stable. if (effectiveSafMode && filePath != null && filePath.isNotEmpty && @@ -5296,8 +5266,6 @@ class DownloadQueueNotifier extends Notifier { ); _failedInSession++; - // Immediately cleanup connections after failure to prevent - // poisoned connection pool from affecting subsequent downloads try { await PlatformBridge.cleanupConnections(); } catch (e) { @@ -5350,7 +5318,6 @@ class DownloadQueueNotifier extends Notifier { ); _failedInSession++; - // Immediately cleanup connections after exception try { await PlatformBridge.cleanupConnections(); } catch (cleanupErr) { diff --git a/lib/providers/explore_provider.dart b/lib/providers/explore_provider.dart index e4ffa5f7..f55892fb 100644 --- a/lib/providers/explore_provider.dart +++ b/lib/providers/explore_provider.dart @@ -11,7 +11,7 @@ final _log = AppLogger('ExploreProvider'); class ExploreItem { final String id; final String uri; - final String type; // track, album, playlist, artist, station + final String type; final String name; final String artists; final String? description; @@ -168,7 +168,6 @@ class ExploreNotifier extends Notifier { return const ExploreState(); } - /// Restore cached home feed from SharedPreferences immediately on startup Future _restoreFromCache() async { try { final prefs = await SharedPreferences.getInstance(); @@ -199,7 +198,6 @@ class ExploreNotifier extends Notifier { } } - /// Save home feed to SharedPreferences for instant restore on next launch Future _saveToCache(List sections) async { try { final prefs = await SharedPreferences.getInstance(); @@ -212,11 +210,9 @@ class ExploreNotifier extends Notifier { } } - /// Fetch home feed from spotify-web extension Future fetchHomeFeed({bool forceRefresh = false}) async { _log.i('fetchHomeFeed called, forceRefresh=$forceRefresh'); - // If we have cached content and it's fresh enough, skip network fetch if (!forceRefresh && state.hasContent && state.lastFetched != null && @@ -230,7 +226,6 @@ class ExploreNotifier extends Notifier { return; } - // Only show loading spinner if we have no cached content to display final showLoading = !state.hasContent; state = state.copyWith(isLoading: showLoading, error: null); @@ -247,14 +242,12 @@ class ExploreNotifier extends Notifier { if (!extension.enabled || !extension.hasHomeFeed) { continue; } - // If user has a preference, use that if (preferredId != null && preferredId.isNotEmpty && extension.id == preferredId) { targetExt = extension; break; } - // Otherwise take the first available (fallback to spotify-web if found) if (targetExt == null || extension.id == 'spotify-web') { targetExt = extension; if (preferredId == null && extension.id == 'spotify-web') { @@ -317,7 +310,6 @@ class ExploreNotifier extends Notifier { lastFetched: DateTime.now(), ); - // Save to disk cache for instant restore on next app launch _saveToCache(sections); } catch (e, stack) { _log.e('Error fetching home feed: $e', e, stack); diff --git a/lib/providers/extension_provider.dart b/lib/providers/extension_provider.dart index 43f25997..3ea5ba7a 100644 --- a/lib/providers/extension_provider.dart +++ b/lib/providers/extension_provider.dart @@ -32,14 +32,12 @@ class Extension { final bool hasMetadataProvider; final bool hasDownloadProvider; final bool hasLyricsProvider; - final bool - skipMetadataEnrichment; // If true, use metadata from extension instead of enriching + final bool skipMetadataEnrichment; final SearchBehavior? searchBehavior; final URLHandler? urlHandler; final TrackMatching? trackMatching; final PostProcessing? postProcessing; - final Map - capabilities; // Extension capabilities (homeFeed, browseCategories, etc.) + final Map capabilities; const Extension({ required this.id, @@ -198,12 +196,10 @@ class SearchBehavior { final String? placeholder; final bool primary; final String? icon; - final String? - thumbnailRatio; // "square" (1:1), "wide" (16:9), "portrait" (2:3) + final String? thumbnailRatio; final int? thumbnailWidth; final int? thumbnailHeight; - final List - filters; // Available search filters (e.g., track, album, artist, playlist) + final List filters; const SearchBehavior({ required this.enabled, @@ -239,11 +235,11 @@ class SearchBehavior { } switch (thumbnailRatio) { - case 'wide': // 16:9 - YouTube style + case 'wide': return (defaultSize * 16 / 9, defaultSize); - case 'portrait': // 2:3 - Poster style + case 'portrait': return (defaultSize * 2 / 3, defaultSize); - case 'square': // 1:1 - Album art style + case 'square': default: return (defaultSize, defaultSize); } @@ -290,7 +286,6 @@ class PostProcessing { } } -/// URL handler configuration for custom URL patterns class URLHandler { final bool enabled; final List patterns; @@ -304,7 +299,6 @@ class URLHandler { ); } - /// Check if a URL matches any of the patterns bool matchesURL(String url) { if (!enabled || patterns.isEmpty) return false; final lowerUrl = url.toLowerCase(); diff --git a/lib/providers/library_collections_provider.dart b/lib/providers/library_collections_provider.dart index a7a3e373..0fa89735 100644 --- a/lib/providers/library_collections_provider.dart +++ b/lib/providers/library_collections_provider.dart @@ -666,7 +666,6 @@ class LibraryCollectionsNotifier extends Notifier { final destPath = p.join(coversDir.path, '$playlistId$ext'); if (playlist.coverImagePath == destPath) return; - // Copy image to persistent location await File(sourceFilePath).copy(destPath); final now = DateTime.now(); @@ -686,7 +685,6 @@ class LibraryCollectionsNotifier extends Notifier { final playlist = state.playlistById(playlistId); if (playlist == null || playlist.coverImagePath == null) return; - // Delete the file if it exists final path = playlist.coverImagePath; if (path != null) { final file = File(path); diff --git a/lib/providers/local_library_provider.dart b/lib/providers/local_library_provider.dart index f63613c5..3e90f0fc 100644 --- a/lib/providers/local_library_provider.dart +++ b/lib/providers/local_library_provider.dart @@ -252,8 +252,6 @@ class LocalLibraryNotifier extends Notifier { _startProgressPolling(); - // On iOS, start accessing the security-scoped bookmark so the Go backend - // can read files outside the app sandbox. String? resolvedPath; bool didStartSecurityAccess = false; if (Platform.isIOS && iosBookmark != null && iosBookmark.isNotEmpty) { @@ -275,9 +273,6 @@ class LocalLibraryNotifier extends Notifier { try { final isSaf = effectiveFolderPath.startsWith('content://'); - // Get all file paths from download history to exclude them. - // Merge DB + in-memory state to avoid race when a fresh download has not - // been flushed to SQLite yet. final downloadedPaths = await _historyDb.getAllFilePaths(); final inMemoryHistoryPaths = ref .read(downloadHistoryProvider) @@ -298,7 +293,6 @@ class LocalLibraryNotifier extends Notifier { ); if (forceFullScan) { - // Full scan path - ignores existing data final results = isSaf ? await PlatformBridge.scanSafTree(effectiveFolderPath) : await PlatformBridge.scanLibraryFolder(effectiveFolderPath); @@ -324,7 +318,6 @@ class LocalLibraryNotifier extends Notifier { _log.i('Skipped $skippedDownloads files already in download history'); } - // Full scan should replace library index atomically. await _db.replaceAll(items.map((e) => e.toJson()).toList()); final persistedItems = [...items]..sort(_compareLibraryItems); @@ -357,7 +350,6 @@ class LocalLibraryNotifier extends Notifier { errorCount: state.scanErrorCount, ); } else { - // Incremental scan path - only scans new/modified files final existingFiles = await _db.getFileModTimes(); _log.i( 'Incremental scan: ${existingFiles.length} existing files in database', @@ -416,7 +408,6 @@ class LocalLibraryNotifier extends Notifier { return; } - // SAF returns 'files' and 'removedUris', non-SAF returns 'scanned' and 'deletedPaths' final scannedList = (result['files'] as List?) ?? (result['scanned'] as List?) ?? @@ -437,10 +428,6 @@ class LocalLibraryNotifier extends Notifier { '$skippedCount skipped, ${deletedPaths.length} deleted, $totalFiles total', ); - // Build the incremental merge base from SQLite, not the current - // provider state. Startup auto-scan can fire before `state.items` has - // finished loading, which would otherwise drop unchanged rows from the - // in-memory library until a manual full rescan. final existingJson = await _db.getAll(); final currentByPath = { for (final item in existingJson.map(LocalLibraryItem.fromJson)) @@ -461,7 +448,6 @@ class LocalLibraryNotifier extends Notifier { ); } - // Upsert new/modified items (excluding downloaded files) final updatedItems = []; int skippedDownloads = existingDownloadedPaths.length; if (scannedList.isNotEmpty) { diff --git a/lib/providers/recent_access_provider.dart b/lib/providers/recent_access_provider.dart index 7b9cc15a..af5e4387 100644 --- a/lib/providers/recent_access_provider.dart +++ b/lib/providers/recent_access_provider.dart @@ -5,18 +5,16 @@ import 'package:spotiflac_android/services/app_state_database.dart'; const _maxRecentItems = 20; -/// Types of items that can be accessed enum RecentAccessType { artist, album, track, playlist } -/// Represents a recently accessed item class RecentAccessItem { final String id; final String name; - final String? subtitle; // Artist name for tracks/albums, null for artists + final String? subtitle; final String? imageUrl; final RecentAccessType type; final DateTime accessedAt; - final String? providerId; // Extension ID or 'deezer' for built-in + final String? providerId; const RecentAccessItem({ required this.id, @@ -53,7 +51,6 @@ class RecentAccessItem { ); } - /// Create a unique key for deduplication String get uniqueKey => '${type.name}:${providerId ?? 'default'}:$id'; @override @@ -67,7 +64,6 @@ class RecentAccessItem { int get hashCode => uniqueKey.hashCode; } -/// State for recent access history class RecentAccessState { final List items; final Set hiddenDownloadIds; @@ -92,7 +88,6 @@ class RecentAccessState { } } -/// Provider for managing recent access history class RecentAccessNotifier extends Notifier { final AppStateDatabase _appStateDb = AppStateDatabase.instance; @@ -135,7 +130,6 @@ class RecentAccessNotifier extends Notifier { } } - /// Record an access to an artist void recordArtistAccess({ required String id, required String name, @@ -154,7 +148,6 @@ class RecentAccessNotifier extends Notifier { ); } - /// Record an access to an album void recordAlbumAccess({ required String id, required String name, @@ -175,7 +168,6 @@ class RecentAccessNotifier extends Notifier { ); } - /// Record an access to a track void recordTrackAccess({ required String id, required String name, @@ -196,7 +188,6 @@ class RecentAccessNotifier extends Notifier { ); } - /// Record an access to a playlist void recordPlaylistAccess({ required String id, required String name, @@ -242,7 +233,6 @@ class RecentAccessNotifier extends Notifier { } } - /// Remove a specific item from history void removeItem(RecentAccessItem item) { final updatedItems = state.items .where((e) => e.uniqueKey != item.uniqueKey) @@ -251,25 +241,21 @@ class RecentAccessNotifier extends Notifier { unawaited(_appStateDb.deleteRecentAccessRow(item.uniqueKey)); } - /// Hide a download item from recents (without deleting the actual download) void hideDownloadFromRecents(String downloadId) { final updatedHidden = {...state.hiddenDownloadIds, downloadId}; state = state.copyWith(hiddenDownloadIds: updatedHidden); unawaited(_appStateDb.addHiddenRecentDownloadId(downloadId)); } - /// Check if a download is hidden from recents bool isDownloadHidden(String downloadId) { return state.hiddenDownloadIds.contains(downloadId); } - /// Clear all history void clearHistory() { state = state.copyWith(items: []); unawaited(_appStateDb.clearRecentAccessRows()); } - /// Clear hidden downloads (show all again) void clearHiddenDownloads() { state = state.copyWith(hiddenDownloadIds: {}); unawaited(_appStateDb.clearHiddenRecentDownloadIds()); diff --git a/lib/providers/store_provider.dart b/lib/providers/store_provider.dart index fc56c011..e6fc5eec 100644 --- a/lib/providers/store_provider.dart +++ b/lib/providers/store_provider.dart @@ -264,13 +264,12 @@ class StoreNotifier extends Notifier { // Read back the resolved URL (may differ from input after normalisation). final resolvedUrl = await PlatformBridge.getStoreRegistryUrl(); - // Persist to SharedPreferences final prefs = await SharedPreferences.getInstance(); await prefs.setString(_registryUrlPrefKey, resolvedUrl); state = state.copyWith( registryUrl: resolvedUrl, - extensions: const [], // Clear old extensions + extensions: const [], ); _log.i('Registry URL set to: $resolvedUrl'); diff --git a/lib/providers/theme_provider.dart b/lib/providers/theme_provider.dart index ad95b21b..0ed2c9e0 100644 --- a/lib/providers/theme_provider.dart +++ b/lib/providers/theme_provider.dart @@ -57,7 +57,6 @@ class ThemeNotifier extends Notifier { await _saveToStorage(); } - /// Set custom seed color (used when dynamic color is disabled) Future setSeedColor(Color color) async { state = state.copyWith(seedColorValue: color.toARGB32()); await _saveToStorage(); @@ -81,4 +80,3 @@ class ThemeNotifier extends Notifier { ); } } - diff --git a/lib/providers/track_provider.dart b/lib/providers/track_provider.dart index 5de7d16b..e3cfaf09 100644 --- a/lib/providers/track_provider.dart +++ b/lib/providers/track_provider.dart @@ -18,21 +18,18 @@ class TrackState { final String? artistId; final String? artistName; final String? coverUrl; - final String? headerImageUrl; // Artist header image for background + final String? headerImageUrl; final int? monthlyListeners; - final List? artistAlbums; // For artist page - final List? artistTopTracks; // Artist's popular tracks - final List? searchArtists; // For search results - final List? searchAlbums; // For search results (albums) - final List? searchPlaylists; // For search results (playlists) - final bool hasSearchText; // For back button handling - final bool isShowingRecentAccess; // For recent access mode - final String? - searchExtensionId; // Extension ID used for current search results - final String? - selectedSearchFilter; // Currently selected search filter (e.g., "track", "album", "artist", "playlist") - final String? - searchSource; // Built-in search provider used for current results (e.g., "deezer", "tidal", "qobuz") + final List? artistAlbums; + final List? artistTopTracks; + final List? searchArtists; + final List? searchAlbums; + final List? searchPlaylists; + final bool hasSearchText; + final bool isShowingRecentAccess; + final String? searchExtensionId; + final String? selectedSearchFilter; + final String? searchSource; const TrackState({ this.tracks = const [], @@ -127,9 +124,9 @@ class ArtistAlbum { final String releaseDate; final int totalTracks; final String? coverUrl; - final String albumType; // album, single, compilation + final String albumType; final String artists; - final String? providerId; // Extension ID if from extension + final String? providerId; const ArtistAlbum({ required this.id, @@ -204,7 +201,6 @@ class TrackNotifier extends Notifier { return const TrackState(); } - /// Check if request is still valid (not cancelled by newer request) bool _isRequestValid(int requestId) => requestId == _currentRequestId; Future fetchFromUrl(String url, {bool useDeezerFallback = true}) async { @@ -217,7 +213,6 @@ class TrackNotifier extends Notifier { if (extensionHandler != null) { _log.i('Found extension URL handler: $extensionHandler for URL: $url'); - // Retry logic for extension URL handlers (up to 3 attempts) Map? result; for (int attempt = 1; attempt <= 3; attempt++) { result = await PlatformBridge.handleURLWithExtension(url); @@ -541,7 +536,6 @@ class TrackNotifier extends Notifier { return; } - // If URL doesn't match any known service, it's unrecognized final isSpotifyUrl = url.contains('open.spotify.com') || url.contains('spotify.link') || @@ -643,7 +637,6 @@ class TrackNotifier extends Notifier { }) async { final requestId = ++_currentRequestId; - // Preserve selected filter during loading final currentFilter = filterOverride ?? state.selectedSearchFilter; state = TrackState( @@ -662,7 +655,6 @@ class TrackNotifier extends Notifier { final includeExtensions = settings.useExtensionProviders && hasActiveMetadataExtensions; - // Determine the effective search provider final effectiveProvider = builtInSearchProvider ?? 'deezer'; _log.i( @@ -672,7 +664,6 @@ class TrackNotifier extends Notifier { Map results; List> metadataTrackResults = []; - // Only use metadata providers for Deezer search (default behavior) if (effectiveProvider == 'deezer') { try { _log.d('Calling metadata provider search API...'); @@ -692,7 +683,6 @@ class TrackNotifier extends Notifier { } } - // Call the appropriate search API switch (effectiveProvider) { case 'tidal': _log.d('Calling Tidal search API...'); @@ -808,9 +798,8 @@ class TrackNotifier extends Notifier { isLoading: false, hasSearchText: state.hasSearchText, isShowingRecentAccess: state.isShowingRecentAccess, - selectedSearchFilter: currentFilter, // Preserve filter in results - searchSource: - effectiveProvider, // Track which service was used for search + selectedSearchFilter: currentFilter, + searchSource: effectiveProvider, ); } catch (e, stackTrace) { if (!_isRequestValid(requestId)) return; @@ -837,7 +826,7 @@ class TrackNotifier extends Notifier { hasSearchText: state.hasSearchText, isShowingRecentAccess: state.isShowingRecentAccess, selectedSearchFilter: - state.selectedSearchFilter, // Preserve filter during loading + state.selectedSearchFilter, ); try { @@ -876,9 +865,8 @@ class TrackNotifier extends Notifier { isLoading: false, hasSearchText: state.hasSearchText, isShowingRecentAccess: state.isShowingRecentAccess, - searchExtensionId: extensionId, // Store which extension was used - selectedSearchFilter: - state.selectedSearchFilter, // Preserve selected filter + searchExtensionId: extensionId, + selectedSearchFilter: state.selectedSearchFilter, ); } catch (e, stackTrace) { if (!_isRequestValid(requestId)) return; @@ -934,7 +922,6 @@ class TrackNotifier extends Notifier { tracks[index] = updatedTrack; state = state.copyWith(tracks: tracks); } catch (_) { - // Silently ignore update failures - track may have been removed } } @@ -942,7 +929,6 @@ class TrackNotifier extends Notifier { state = const TrackState(); } - /// Set selected search filter for extension search void setSearchFilter(String? filter) { if (state.selectedSearchFilter == filter) return; state = state.copyWith( @@ -951,7 +937,6 @@ class TrackNotifier extends Notifier { ); } - /// Set search text state for back button handling void setSearchText(bool hasText) { if (state.hasSearchText == hasText) { return; @@ -966,7 +951,6 @@ class TrackNotifier extends Notifier { state = state.copyWith(isShowingRecentAccess: showing); } - /// Set tracks from a collection (album/playlist) opened from search results void setTracksFromCollection({ required List tracks, String? albumName, @@ -1127,7 +1111,7 @@ class TrackNotifier extends Notifier { 'isrc': isrc, 'track_name': track.name, 'artist_name': track.artistName, - 'spotify_id': track.id, // Include Spotify ID for Amazon lookup + 'spotify_id': track.id, 'service': 'tidal', }); if (cacheRequests.length >= _maxPreWarmTracksPerRequest) { diff --git a/lib/screens/downloaded_album_screen.dart b/lib/screens/downloaded_album_screen.dart index 2690fb33..ca41f0f1 100644 --- a/lib/screens/downloaded_album_screen.dart +++ b/lib/screens/downloaded_album_screen.dart @@ -1105,7 +1105,6 @@ class _DownloadedAlbumScreenState extends ConsumerState { ? 'Opus' : null; if (ext == null || ext == targetFormat) continue; - // Skip lossy sources when target is lossless (pointless re-encoding) final isLosslessTarget = targetFormat == 'ALAC' || targetFormat == 'FLAC'; final isLosslessSource = ext == 'FLAC' || ext == 'M4A'; if (isLosslessTarget && !isLosslessSource) continue; diff --git a/lib/screens/local_album_screen.dart b/lib/screens/local_album_screen.dart index d41e6c80..ed0c6e36 100644 --- a/lib/screens/local_album_screen.dart +++ b/lib/screens/local_album_screen.dart @@ -1367,7 +1367,6 @@ class _LocalAlbumScreenState extends ConsumerState { } } if (currentFormat == null || currentFormat == targetFormat) continue; - // Skip lossy sources when target is lossless (pointless re-encoding) final isLosslessTarget = targetFormat == 'ALAC' || targetFormat == 'FLAC'; final isLosslessSource = currentFormat == 'FLAC' || currentFormat == 'M4A'; @@ -1488,7 +1487,7 @@ class _LocalAlbumScreenState extends ConsumerState { bitrate: bitrate, metadata: metadata, coverPath: coverPath, - deleteOriginal: !isSaf, // Only delete original for regular files + deleteOriginal: !isSaf, ); if (coverPath != null) { @@ -1507,15 +1506,9 @@ class _LocalAlbumScreenState extends ConsumerState { } if (isSaf) { - // For SAF: derive the parent tree URI and relative dir from the content URI, - // then create new SAF file and delete old one - // Parse the SAF URI to get the tree document path: - // content://...tree/...document/.../oldName.flac - // We need tree URI and relative dir to create the new file final uri = Uri.parse(item.filePath); final pathSegments = uri.pathSegments; - // Try to find 'tree' and 'document' segments String? treeUri; String relativeDir = ''; String oldFileName = ''; diff --git a/lib/screens/main_shell.dart b/lib/screens/main_shell.dart index cb292291..49bbe89c 100644 --- a/lib/screens/main_shell.dart +++ b/lib/screens/main_shell.dart @@ -162,7 +162,6 @@ class _MainShellState extends ConsumerState if (!Platform.isAndroid) return; final settings = ref.read(settingsProvider); - // Only show if user is still on legacy storage mode with a download dir set if (settings.storageMode == 'saf') return; if (settings.downloadDirectory.isEmpty) return; diff --git a/lib/screens/queue_tab.dart b/lib/screens/queue_tab.dart index 326c78e4..4b2a5cb0 100644 --- a/lib/screens/queue_tab.dart +++ b/lib/screens/queue_tab.dart @@ -97,7 +97,6 @@ class UnifiedLibraryItem { } else if (item.bitDepth != null && item.bitDepth! > 0 && item.sampleRate != null) { - // Lossless format with actual bit depth quality = buildDisplayAudioQuality( bitDepth: item.bitDepth, sampleRate: item.sampleRate, @@ -108,7 +107,7 @@ class UnifiedLibraryItem { trackName: item.trackName, artistName: item.artistName, albumName: item.albumName, - coverUrl: null, // Local library doesn't have cover URLs + coverUrl: null, localCoverPath: item.coverPath, filePath: item.filePath, quality: quality, @@ -170,9 +169,6 @@ class UnifiedLibraryItem { } if (localItem != null) { final l = localItem!; - // Store coverPath (even local file paths) in coverUrl so playlist - // entries retain the cover. All renderers must check whether the - // value is a URL or a local path and use the appropriate widget. return Track( id: l.id, name: l.trackName, @@ -188,7 +184,6 @@ class UnifiedLibraryItem { source: 'local', ); } - // Fallback — should not happen return Track( id: id, name: trackName, @@ -4889,15 +4884,12 @@ class _QueueTabState extends ConsumerState { for (final id in _selectedIds) { final item = itemsById[id]; if (item == null) continue; - // Detect format: use safFileName for download history SAF items, - // item.localItem?.format for local library items, file extension as fallback String nameToCheck; if (item.historyItem?.safFileName != null && item.historyItem!.safFileName!.isNotEmpty) { nameToCheck = item.historyItem!.safFileName!.toLowerCase(); } else if (item.localItem?.format != null && item.localItem!.format!.isNotEmpty) { - // Synthesize a fake extension to keep detection unified nameToCheck = '.${item.localItem!.format!.toLowerCase()}'; } else { nameToCheck = item.filePath.toLowerCase(); @@ -4912,7 +4904,6 @@ class _QueueTabState extends ConsumerState { ? 'Opus' : null; if (ext == null || ext == targetFormat) continue; - // Skip lossy sources when target is lossless (pointless re-encoding) final isLosslessTarget = targetFormat == 'ALAC' || targetFormat == 'FLAC'; final isLosslessSource = ext == 'FLAC' || ext == 'M4A'; if (isLosslessTarget && !isLosslessSource) continue; @@ -5060,7 +5051,6 @@ class _QueueTabState extends ConsumerState { continue; } - // Handle SAF write-back if (isSaf && item.historyItem != null) { final hi = item.historyItem!; final treeUri = hi.downloadTreeUri; @@ -5113,12 +5103,10 @@ class _QueueTabState extends ConsumerState { continue; } - // Delete old SAF file try { await PlatformBridge.safDelete(item.filePath); } catch (_) {} - // Update history await historyDb.updateFilePath( hi.id, safUri, @@ -5127,7 +5115,6 @@ class _QueueTabState extends ConsumerState { clearAudioSpecs: true, ); } - // Cleanup temp files try { await File(newPath).delete(); } catch (_) {} @@ -5137,7 +5124,6 @@ class _QueueTabState extends ConsumerState { } catch (_) {} } } else if (isSaf && item.localItem != null) { - // Local library SAF item: parse content URI to derive tree and dir final uri = Uri.parse(item.filePath); final pathSegments = uri.pathSegments; @@ -5214,7 +5200,6 @@ class _QueueTabState extends ConsumerState { await LibraryDatabase.instance.deleteByPath(item.filePath); } - // Cleanup temp files try { await File(newPath).delete(); } catch (_) {} @@ -5224,7 +5209,6 @@ class _QueueTabState extends ConsumerState { } catch (_) {} } } else if (item.historyItem != null) { - // Regular file - update history path await historyDb.updateFilePath( item.historyItem!.id, newPath, @@ -5232,17 +5216,13 @@ class _QueueTabState extends ConsumerState { clearAudioSpecs: true, ); } else if (item.localItem != null) { - // Regular local library file - delete old db entry, rescan picks up new file await LibraryDatabase.instance.deleteByPath(item.filePath); } successCount++; - } catch (_) { - // Continue to next item on error - } + } catch (_) {} } - // Reload history and local library to reflect path changes in UI ref.read(downloadHistoryProvider.notifier).reloadFromStorage(); ref.read(localLibraryProvider.notifier).reloadFromStorage(); @@ -5264,7 +5244,6 @@ class _QueueTabState extends ConsumerState { } } - /// Bottom action bar for selection mode (Material Design 3 style) Widget _buildSelectionBottomBar( BuildContext context, ColorScheme colorScheme, diff --git a/lib/screens/settings/extension_detail_page.dart b/lib/screens/settings/extension_detail_page.dart index c6e00b55..f2f0b950 100644 --- a/lib/screens/settings/extension_detail_page.dart +++ b/lib/screens/settings/extension_detail_page.dart @@ -61,7 +61,7 @@ class _ExtensionDetailPageState extends ConsumerState { final hasError = extension.status == 'error'; return PopScope( - canPop: true, // Always allow back gesture + canPop: true, child: Scaffold( body: CustomScrollView( slivers: [ diff --git a/lib/screens/settings/extensions_page.dart b/lib/screens/settings/extensions_page.dart index 57979689..7e5aa26e 100644 --- a/lib/screens/settings/extensions_page.dart +++ b/lib/screens/settings/extensions_page.dart @@ -61,7 +61,7 @@ class _ExtensionsPageState extends ConsumerState { final topPadding = normalizedHeaderTopPadding(context); return PopScope( - canPop: true, // Always allow back gesture + canPop: true, child: Scaffold( body: CustomScrollView( slivers: [ @@ -600,14 +600,12 @@ class _SearchProviderSelector extends ConsumerWidget { .where((e) => e.enabled && e.hasCustomSearch) .toList(); - // Always allow tapping: built-in providers are always available final hasAnyProvider = searchProviders.isNotEmpty || _builtInProviders.isNotEmpty; String currentProviderName = context.l10n.extensionDefaultProvider; if (settings.searchProvider != null && settings.searchProvider!.isNotEmpty) { - // Check built-in first if (_builtInProviders.containsKey(settings.searchProvider)) { currentProviderName = _builtInProviders[settings.searchProvider]!; } else { diff --git a/lib/screens/settings/library_settings_page.dart b/lib/screens/settings/library_settings_page.dart index 86c461a3..eae12522 100644 --- a/lib/screens/settings/library_settings_page.dart +++ b/lib/screens/settings/library_settings_page.dart @@ -23,21 +23,15 @@ class _LibrarySettingsPageState extends ConsumerState { int _androidSdkVersion = 0; bool _hasStoragePermission = false; - /// Convert SAF content URI to a readable display path String _getDisplayPath(String path) { if (!path.startsWith('content://')) return path; - // Extract the path portion from SAF tree URI - // e.g. content://com.android.externalstorage.documents/tree/primary%3AMusic - // -> /storage/emulated/0/Music try { final uri = Uri.parse(path); - final treePath = - uri.pathSegments.last; // e.g. "primary:Music" or "primary%3AMusic" + final treePath = uri.pathSegments.last; final decoded = Uri.decodeComponent(treePath); if (decoded.startsWith('primary:')) { return '/storage/emulated/0/${decoded.substring('primary:'.length)}'; } - // For SD card or other volumes, just show the decoded path return decoded; } catch (_) { return path; diff --git a/lib/screens/settings/log_screen.dart b/lib/screens/settings/log_screen.dart index 611db06f..b34e172a 100644 --- a/lib/screens/settings/log_screen.dart +++ b/lib/screens/settings/log_screen.dart @@ -136,7 +136,7 @@ class _LogScreenState extends State { final logs = _filteredLogs; return PopScope( - canPop: true, // Always allow back gesture + canPop: true, child: Scaffold( body: CustomScrollView( controller: _scrollController, diff --git a/lib/screens/settings/options_settings_page.dart b/lib/screens/settings/options_settings_page.dart index 3f5a2e7c..1025f1b1 100644 --- a/lib/screens/settings/options_settings_page.dart +++ b/lib/screens/settings/options_settings_page.dart @@ -19,7 +19,7 @@ class OptionsSettingsPage extends ConsumerWidget { final topPadding = normalizedHeaderTopPadding(context); return PopScope( - canPop: true, // Always allow back gesture + canPop: true, child: Scaffold( body: CustomScrollView( slivers: [ diff --git a/lib/screens/setup_screen.dart b/lib/screens/setup_screen.dart index 5f137482..bc72a18d 100644 --- a/lib/screens/setup_screen.dart +++ b/lib/screens/setup_screen.dart @@ -441,14 +441,9 @@ class _SetupScreenState extends ConsumerState { void _nextPage() { bool canProceed = false; - // Step 0 is Welcome, always can proceed if (_currentStep == 0) { canProceed = true; } else { - // Logic for other steps (offset by 1 because of welcome step) - // Step 1: Storage - // Step 2: Notification (if android 13+) OR Directory - // etc. canProceed = _isStepCompleted(_currentStep); } @@ -470,9 +465,8 @@ class _SetupScreenState extends ConsumerState { } bool _isStepCompleted(int step) { - if (step == 0) return true; // Welcome + if (step == 0) return true; - // Adjust step index for logic because we added Welcome at 0 final logicStep = step - 1; if (_androidSdkVersion >= 33) { diff --git a/lib/screens/track_metadata_screen.dart b/lib/screens/track_metadata_screen.dart index d84b9fc9..254d2f06 100644 --- a/lib/screens/track_metadata_screen.dart +++ b/lib/screens/track_metadata_screen.dart @@ -60,19 +60,19 @@ class _TrackMetadataScreenState extends ConsumerState { bool _fileExists = false; bool _hasCheckedFile = false; int? _fileSize; - String? _lyrics; // Cleaned lyrics for display (no timestamps) - String? _rawLyrics; // Raw LRC with timestamps for embedding + String? _lyrics; + String? _rawLyrics; bool _lyricsLoading = false; String? _lyricsError; String? _lyricsSource; bool _showTitleInAppBar = false; bool _lyricsEmbedded = false; - bool _isEmbedding = false; // Track embed operation in progress + bool _isEmbedding = false; bool _isInstrumental = false; - bool _isConverting = false; // Track convert operation in progress + bool _isConverting = false; bool _hasMetadataChanges = false; bool _hasLoadedResolvedAudioMetadata = false; - Map? _editedMetadata; // Overrides after metadata edit + Map? _editedMetadata; String? _embeddedCoverPreviewPath; final ScrollController _scrollController = ScrollController(); static final RegExp _lrcTimestampPattern = RegExp( @@ -577,7 +577,6 @@ class _TrackMetadataScreenState extends ConsumerState { String get cleanFilePath { var path = _filePath; if (path.startsWith('EXISTS:')) path = path.substring(7); - // Strip CUE virtual path suffix for filesystem operations if (isCueVirtualPath(path)) path = stripCueTrackSuffix(path); return path; } @@ -1707,7 +1706,7 @@ class _TrackMetadataScreenState extends ConsumerState { _spotifyId ?? '', trackName, artistName, - filePath: null, // Don't check file again + filePath: null, durationMs: durationMs, ).timeout(const Duration(seconds: 20)); @@ -1733,9 +1732,9 @@ class _TrackMetadataScreenState extends ConsumerState { final cleanLyrics = _cleanLrcForDisplay(lrcText); setState(() { _lyrics = cleanLyrics; - _rawLyrics = lrcText; // Keep raw LRC with timestamps for embedding + _rawLyrics = lrcText; _lyricsSource = source.isNotEmpty ? source : null; - _lyricsEmbedded = false; // Lyrics from online, not embedded + _lyricsEmbedded = false; _lyricsLoading = false; }); } @@ -1762,7 +1761,6 @@ class _TrackMetadataScreenState extends ConsumerState { setState(() => _isEmbedding = true); - // Capture l10n strings before async gaps to avoid use_build_context_synchronously final l10nFailedToWriteStorage = context.l10n.snackbarFailedToWriteStorage; final l10nFailedToEmbedLyrics = context.l10n.snackbarFailedToEmbedLyrics; final l10nUnsupportedFormat = context.l10n.snackbarUnsupportedAudioFormat; @@ -3556,7 +3554,7 @@ class _TrackMetadataScreenState extends ConsumerState { bitrate: bitrate, metadata: metadata, coverPath: coverPath, - deleteOriginal: !isSaf, // Don't delete temp copy for SAF, we handle it + deleteOriginal: !isSaf, ); if (coverPath != null) { @@ -3627,7 +3625,7 @@ class _TrackMetadataScreenState extends ConsumerState { newExt = '.flac'; mimeType = 'audio/flac'; break; - default: // mp3 + default: newExt = '.mp3'; mimeType = 'audio/mpeg'; break; diff --git a/lib/services/csv_import_service.dart b/lib/services/csv_import_service.dart index 6cb77e0d..551acd17 100644 --- a/lib/services/csv_import_service.dart +++ b/lib/services/csv_import_service.dart @@ -198,8 +198,8 @@ class CsvImportService { artistName: artistName ?? 'Unknown Artist', albumName: albumName ?? 'Unknown Album', isrc: isrc, - duration: 0, // Will be updated by enrichment later - coverUrl: null, // Will be fetched by enrichment + duration: 0, + coverUrl: null, ), ); } diff --git a/lib/services/ffmpeg_service.dart b/lib/services/ffmpeg_service.dart index 53efe84d..801ff147 100644 --- a/lib/services/ffmpeg_service.dart +++ b/lib/services/ffmpeg_service.dart @@ -1437,7 +1437,6 @@ class FFmpegService { final cmdBuffer = StringBuffer(); cmdBuffer.write('-i "$inputPath" '); - // Cover art as second input for M4A attached picture final hasCover = coverPath != null && coverPath.trim().isNotEmpty && @@ -1455,7 +1454,6 @@ class FFmpegService { cmdBuffer.write('-c:a alac '); cmdBuffer.write('-map_metadata -1 '); - // Embed M4A metadata tags final m4aTags = _convertToM4aTags(metadata); for (final entry in m4aTags.entries) { final sanitized = entry.value.replaceAll('"', '\\"'); @@ -1764,7 +1762,6 @@ class FFmpegService { final outputPaths = []; final inputExt = audioPath.toLowerCase().split('.').last; - // For lossless formats, keep as FLAC; for others, keep original format final outputExt = (inputExt == 'flac' || inputExt == 'wav' || @@ -1836,14 +1833,10 @@ class FFmpegService { final result = await _execute(command); if (!result.success) { _log.e('CUE split failed for track ${track.number}: ${result.output}'); - // Continue with remaining tracks instead of failing completely continue; } - // Embed cover art if available (for FLAC output) if (coverPath != null && coverPath.isNotEmpty && outputExt == 'flac') { - // Use the Go backend for FLAC cover embedding via PlatformBridge - // (handled by the caller) } outputPaths.add(outputPath); diff --git a/lib/services/platform_bridge.dart b/lib/services/platform_bridge.dart index 313f2f40..401a3afc 100644 --- a/lib/services/platform_bridge.dart +++ b/lib/services/platform_bridge.dart @@ -1081,7 +1081,6 @@ class PlatformBridge { } } - /// Set the directory for caching extracted cover art static Future setLibraryCoverCacheDir(String cacheDir) async { _log.i('setLibraryCoverCacheDir: $cacheDir'); await _channel.invokeMethod('setLibraryCoverCacheDir', { @@ -1089,8 +1088,6 @@ class PlatformBridge { }); } - /// Scan a folder for audio files and read their metadata - /// Returns a list of track metadata static Future>> scanLibraryFolder( String folderPath, ) async { @@ -1102,10 +1099,6 @@ class PlatformBridge { return list.map((e) => e as Map).toList(); } - /// Perform an incremental scan of the library folder - /// Only scans files that are new or have changed since last scan - /// [existingFiles] is a map of filePath -> modTime (unix millis) - /// Returns IncrementalScanResult with scanned items, deleted paths, and skip count static Future> scanLibraryFolderIncremental( String folderPath, Map existingFiles, @@ -1140,8 +1133,6 @@ class PlatformBridge { return list.map((e) => e as Map).toList(); } - /// Incremental SAF tree scan - only scans new or modified files - /// Returns a map with 'files' (new/changed) and 'removedUris' (deleted files) static Future> scanSafTreeIncremental( String treeUri, Map existingFiles, @@ -1167,8 +1158,6 @@ class PlatformBridge { return jsonDecode(result as String) as Map; } - /// Get last-modified timestamps for a list of SAF file URIs. - /// Returns map uri -> modTime (unix millis), only for files that still exist. static Future> getSafFileModTimes(List uris) async { final result = await _channel.invokeMethod('getSafFileModTimes', { 'uris': jsonEncode(uris), @@ -1177,7 +1166,6 @@ class PlatformBridge { return map.map((key, value) => MapEntry(key, (value as num).toInt())); } - /// Get current library scan progress static Future> getLibraryScanProgress() async { final result = await _channel.invokeMethod('getLibraryScanProgress'); return _decodeMapResult(result); @@ -1189,7 +1177,6 @@ class PlatformBridge { ); } - /// Cancel ongoing library scan static Future cancelLibraryScan() async { await _channel.invokeMethod('cancelLibraryScan'); } @@ -1249,7 +1236,6 @@ class PlatformBridge { } } - /// Read metadata from a single audio file static Future?> readAudioMetadata( String filePath, ) async { @@ -1369,10 +1355,6 @@ class PlatformBridge { await _channel.invokeMethod('clearStoreCache'); } - /// Parse a .cue file and return split information (track listing, timing, metadata). - /// Returns a map with: cue_path, audio_path, album, artist, genre, date, tracks[] - /// Each track has: number, title, artist, isrc, composer, start_sec, end_sec - /// [audioDir] optionally overrides the directory for audio file resolution (used for SAF). static Future> parseCueSheet( String cuePath, { String audioDir = '', diff --git a/lib/services/share_intent_service.dart b/lib/services/share_intent_service.dart index e958394f..1040c738 100644 --- a/lib/services/share_intent_service.dart +++ b/lib/services/share_intent_service.dart @@ -80,7 +80,6 @@ class ShareIntentService { bool isInitial = false, }) { for (final file in files) { - // Check both path and message - apps may share URL in either field final textsToCheck = [file.path, if (file.message != null) file.message!]; for (final textToCheck in textsToCheck) { @@ -100,13 +99,11 @@ class ShareIntentService { String? _extractMusicUrl(String text) { if (text.isEmpty) return null; - // Try Spotify URI first final uriMatch = _spotifyUriPattern.firstMatch(text); if (uriMatch != null) { return uriMatch.group(0); } - // Try all URL patterns final patterns = [ _spotifyUrlPattern, _deezerUrlPattern, diff --git a/lib/utils/clickable_metadata.dart b/lib/utils/clickable_metadata.dart index a4ec68ca..91e40501 100644 --- a/lib/utils/clickable_metadata.dart +++ b/lib/utils/clickable_metadata.dart @@ -27,7 +27,6 @@ Future navigateToArtist( final normalizedArtistId = _normalizeArtistId(artistId); - // If we have a valid artist ID already, navigate directly if (normalizedArtistId != null && _canNavigateArtistDirectly( artistId: normalizedArtistId, @@ -43,7 +42,6 @@ Future navigateToArtist( return; } - // Search Deezer to resolve the artist ID _showLoadingSnackBar(context, 'Looking up artist...'); try { final results = await PlatformBridge.searchDeezerAll( @@ -60,7 +58,6 @@ Future navigateToArtist( return; } - // Find best match - prefer exact name match (case-insensitive) Map? bestMatch; final lowerName = artistName.toLowerCase().trim(); for (final a in artistList) { @@ -113,7 +110,6 @@ Future navigateToAlbum( }) async { if (albumName.isEmpty) return; - // If we have a valid album ID already, navigate directly if (albumId != null && albumId.isNotEmpty && albumId != 'unknown' && @@ -128,16 +124,13 @@ Future navigateToAlbum( return; } - // If it's extension-based content without an ID, can't search Deezer for it if (extensionId != null) { _showUnavailable(context, 'Album'); return; } - // Search Deezer to resolve the album ID _showLoadingSnackBar(context, 'Looking up album...'); try { - // Build search query: "albumName artistName" for better accuracy final query = artistName != null && artistName.isNotEmpty ? '$albumName $artistName' : albumName; @@ -156,7 +149,6 @@ Future navigateToAlbum( return; } - // Find best match - prefer exact name match (case-insensitive) Map? bestMatch; final lowerName = albumName.toLowerCase().trim(); for (final a in albumList) { diff --git a/lib/widgets/audio_analysis_widget.dart b/lib/widgets/audio_analysis_widget.dart index 396adde9..8aac0215 100644 --- a/lib/widgets/audio_analysis_widget.dart +++ b/lib/widgets/audio_analysis_widget.dart @@ -14,8 +14,6 @@ import 'package:path_provider/path_provider.dart'; import 'package:spotiflac_android/l10n/l10n.dart'; import 'package:spotiflac_android/services/platform_bridge.dart'; -// Data models - class AudioAnalysisData { final String filePath; final int fileSize; @@ -98,8 +96,6 @@ class SpectrogramData { }); } -// Audio Analysis Card Widget - class AudioAnalysisCard extends StatefulWidget { final String filePath; @@ -179,7 +175,6 @@ class _AudioAnalysisCardState extends State { }); try { - // Try loading from cache first final cached = await _loadFromCache(widget.filePath); AudioAnalysisData data; @@ -187,7 +182,6 @@ class _AudioAnalysisCardState extends State { data = cached; } else { data = await _runAnalysis(widget.filePath); - // Save to cache (fire-and-forget) _saveToCache(widget.filePath, data); } @@ -214,8 +208,6 @@ class _AudioAnalysisCardState extends State { } } - // Analysis cache - static String _cacheKey(String filePath) { var hash = 0xcbf29ce484222325; for (final byte in utf8.encode(filePath)) { @@ -267,8 +259,6 @@ class _AudioAnalysisCardState extends State { } catch (_) {} } - // Analysis pipeline - Future _runAnalysis(String filePath) async { await FFmpegKitConfig.setLogLevel(Level.avLogError); @@ -302,7 +292,6 @@ class _AudioAnalysisCardState extends State { ), ); - // Total samples from file metadata (not truncated PCM) final trueTotalSamples = (info.duration * info.sampleRate * info.channels).round(); @@ -468,7 +457,6 @@ class _AudioAnalysisCardState extends State { final cs = Theme.of(context).colorScheme; final l10n = context.l10n; - // Still checking cache, show nothing yet if (_checkingCache) return const SizedBox.shrink(); if (_analyzing) { @@ -575,8 +563,6 @@ class _AudioAnalysisCardState extends State { } } -// Internal types - class _MediaInfo { final int fileSize; final int sampleRate; @@ -623,8 +609,6 @@ class _AnalysisResult { }); } -// Isolate: PCM analysis + FFT spectrogram - _AnalysisResult _analyzeInIsolate(_AnalysisParams params) { final byteData = ByteData.sublistView(params.pcmBytes); final sampleCount = params.pcmBytes.length ~/ 2; @@ -767,8 +751,6 @@ Float64List _fft(Float64List realInput) { return data; } -// Audio Info Card - class _AudioInfoCard extends StatelessWidget { final AudioAnalysisData data; @@ -945,8 +927,6 @@ class _MetricChip extends StatelessWidget { } } -// Spectrogram View - class _SpectrogramView extends StatelessWidget { final ui.Image image; final SpectrogramData spectrum; @@ -1011,8 +991,6 @@ class _ImagePainter extends CustomPainter { bool shouldRepaint(covariant _ImagePainter old) => old.image != image; } -// Spectrogram pixel-buffer rendering (runs in isolate) - class _SpectrogramRenderParams { final SpectrogramData spectrum; final int width; @@ -1031,7 +1009,6 @@ Uint8List _renderSpectrogramPixels(_SpectrogramRenderParams params) { final spectrum = params.spectrum; final pixels = Uint8List(w * h * 4); - // Fill black for (int i = 3; i < pixels.length; i += 4) { pixels[i] = 255; } @@ -1041,7 +1018,6 @@ Uint8List _renderSpectrogramPixels(_SpectrogramRenderParams params) { final freqBins = spectrum.freqBins; - // dB range double minDB = 0; double maxDB = -200; for (final slice in slices) { diff --git a/lib/widgets/donate_icons.dart b/lib/widgets/donate_icons.dart index 21ba4195..fa34125a 100644 --- a/lib/widgets/donate_icons.dart +++ b/lib/widgets/donate_icons.dart @@ -166,7 +166,6 @@ class _GitHubPainter extends CustomPainter { 9.47 * scale, 17.93 * scale, 9.81 * scale, 17.63 * scale, ); - // Bottom path.cubicTo( 7.15 * scale, 17.33 * scale, 4.34 * scale, 16.33 * scale,