From dbd32baa1245da55e0d00966a4194a9716625997 Mon Sep 17 00:00:00 2001 From: eddyizm Date: Wed, 11 Feb 2026 21:31:46 -0800 Subject: [PATCH] feat: prefer locally downloaded media vs server stream (#433) resolves #404 and should address #285 --- app/build.gradle | 2 +- .../ui/fragment/PlayerControllerFragment.java | 58 +++++++++++++------ .../tempo/util/MappingUtil.java | 19 ++++-- .../tempo/util/MusicUtil.java | 25 ++++++-- .../tempo/util/TranscodingMediaSource.kt | 8 ++- 5 files changed, 84 insertions(+), 28 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 99691cf3..ec565876 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -11,7 +11,7 @@ android { targetSdk 35 versionCode 19 - versionName '4.10.1' + versionName '4.10.2' testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' javaCompileOptions { diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerControllerFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerControllerFragment.java index c6a0d6fd..7f99b0d9 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerControllerFragment.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerControllerFragment.java @@ -7,6 +7,7 @@ import android.content.ServiceConnection; import android.os.Bundle; import android.os.IBinder; import android.text.TextUtils; +import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -236,43 +237,64 @@ public class PlayerControllerFragment extends Fragment { } private void setMediaInfo(MediaMetadata mediaMetadata) { + boolean isLocal = false; + + if (mediaBrowserListenableFuture != null && mediaBrowserListenableFuture.isDone()) { + try { + MediaBrowser browser = mediaBrowserListenableFuture.get(); + if (browser != null && browser.getCurrentMediaItem() != null) { + android.net.Uri currentUri = browser.getCurrentMediaItem().requestMetadata.mediaUri; + if (currentUri != null) { + String scheme = currentUri.getScheme(); + isLocal = "content".equals(scheme) || "file".equals(scheme); + } + } + } catch (Exception e) { + Log.e("DEBUG_PLAYER", "Error getting browser for UI update", e); + } + } + if (mediaMetadata.extras != null) { String extension = mediaMetadata.extras.getString("suffix", getString(R.string.player_unknown_format)); - String bitrate = mediaMetadata.extras.getInt("bitrate", 0) != 0 ? mediaMetadata.extras.getInt("bitrate", 0) + "kbps" : "Original"; - String samplingRate = mediaMetadata.extras.getInt("samplingRate", 0) != 0 ? new DecimalFormat("0.#").format(mediaMetadata.extras.getInt("samplingRate", 0) / 1000.0) + "kHz" : ""; + int rawBitrate = mediaMetadata.extras.getInt("bitrate", 0); + String bitrate = rawBitrate != 0 ? rawBitrate + "kbps" : "Original"; + String samplingRate = mediaMetadata.extras.getInt("samplingRate", 0) != 0 ? + new java.text.DecimalFormat("0.#").format(mediaMetadata.extras.getInt("samplingRate", 0) / 1000.0) + "kHz" : ""; String bitDepth = mediaMetadata.extras.getInt("bitDepth", 0) != 0 ? mediaMetadata.extras.getInt("bitDepth", 0) + "b" : ""; playerMediaExtension.setText(extension); - if (bitrate.equals("Original")) { + if (bitrate.equals("Original") && !isLocal) { playerMediaBitrate.setVisibility(View.GONE); } else { - List mediaQualityItems = new ArrayList<>(); - - if (!bitrate.trim().isEmpty()) mediaQualityItems.add(bitrate); - if (!bitDepth.trim().isEmpty()) mediaQualityItems.add(bitDepth); - if (!samplingRate.trim().isEmpty()) mediaQualityItems.add(samplingRate); - - String mediaQuality = TextUtils.join(" • ", mediaQualityItems); + List items = new ArrayList<>(); + if (!bitrate.trim().isEmpty()) items.add(bitrate); + if (!bitDepth.trim().isEmpty()) items.add(bitDepth); + if (!samplingRate.trim().isEmpty()) items.add(samplingRate); + String mediaQuality = TextUtils.join(" • ", items); + playerMediaBitrate.setVisibility(View.VISIBLE); - playerMediaBitrate.setText(mediaQuality); + playerMediaBitrate.setText(isLocal ? mediaQuality : mediaQuality); } } - boolean isTranscodingExtension = !MusicUtil.getTranscodingFormatPreference().equals("raw"); - boolean isTranscodingBitrate = !MusicUtil.getBitratePreference().equals("0"); + + if (!isLocal) { + boolean isTranscodingExtension = !MusicUtil.getTranscodingFormatPreference().equals("raw"); + boolean isTranscodingBitrate = !MusicUtil.getBitratePreference().equals("0"); + if (isTranscodingExtension || isTranscodingBitrate) { + playerMediaExtension.setText(MusicUtil.getTranscodingFormatPreference() + " (" + getString(R.string.player_transcoding) + ")"); + playerMediaBitrate.setText(!MusicUtil.getBitratePreference().equals("0") ? + MusicUtil.getBitratePreference() + "kbps" : getString(R.string.player_transcoding_requested)); + } - if (isTranscodingExtension || isTranscodingBitrate) { - playerMediaExtension.setText(MusicUtil.getTranscodingFormatPreference() + " (" + getString(R.string.player_transcoding) + ")"); - playerMediaBitrate.setText(!MusicUtil.getBitratePreference().equals("0") ? MusicUtil.getBitratePreference() + "kbps" : getString(R.string.player_transcoding_requested)); } playerTrackInfo.setOnClickListener(view -> { TrackInfoDialog dialog = new TrackInfoDialog(mediaMetadata); dialog.show(activity.getSupportFragmentManager(), null); - }); + }); } - private void updateAssetLinkChips(MediaMetadata mediaMetadata) { if (assetLinkChipGroup == null) return; String mediaType = mediaMetadata.extras != null ? mediaMetadata.extras.getString("type", Constants.MEDIA_TYPE_MUSIC) : Constants.MEDIA_TYPE_MUSIC; diff --git a/app/src/main/java/com/cappielloantonio/tempo/util/MappingUtil.java b/app/src/main/java/com/cappielloantonio/tempo/util/MappingUtil.java index 71007c78..1cad9e36 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/util/MappingUtil.java +++ b/app/src/main/java/com/cappielloantonio/tempo/util/MappingUtil.java @@ -288,13 +288,24 @@ public class MappingUtil { } private static Uri getUri(Child media) { + // Check if it's in our local SQL Database + DownloadRepository repo = new DownloadRepository(); + Download localDownload = repo.getDownload(media.getId()); + + if (localDownload != null && localDownload.getDownloadUri() != null && !localDownload.getDownloadUri().isEmpty()) { + Log.d(TAG, "Playing local file for: " + media.getTitle()); + return Uri.parse(localDownload.getDownloadUri()); + } + + // Legacy check for external directory, i think this was broken/buggy if (Preferences.getDownloadDirectoryUri() != null) { Uri local = ExternalAudioReader.getUri(media); - return local != null ? local : MusicUtil.getStreamUri(media.getId()); + if (local != null) return local; } - return DownloadUtil.getDownloadTracker(App.getContext()).isDownloaded(media.getId()) - ? getDownloadUri(media.getId()) - : MusicUtil.getStreamUri(media.getId()); + + // Fallback to streaming + Log.d(TAG, "No local file found. Streaming: " + media.getTitle()); + return MusicUtil.getStreamUri(media.getId()); } private static Uri getUri(PodcastEpisode podcastEpisode) { diff --git a/app/src/main/java/com/cappielloantonio/tempo/util/MusicUtil.java b/app/src/main/java/com/cappielloantonio/tempo/util/MusicUtil.java index c1656899..e04a268a 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/util/MusicUtil.java +++ b/app/src/main/java/com/cappielloantonio/tempo/util/MusicUtil.java @@ -52,6 +52,10 @@ public class MusicUtil { if (params.containsKey("c") && params.get("c") != null) uri.append("&c=").append(params.get("c")); + String selectedBitrate = getBitratePreference(); + String selectedFormat = getTranscodingFormatPreference(); + Log.i(TAG, "DEBUG: Requesting Format: " + selectedFormat + " at Bitrate: " + selectedBitrate); + if (!Preferences.isServerPrioritized()) uri.append("&maxBitRate=").append(getBitratePreference()); if (!Preferences.isServerPrioritized()) @@ -73,7 +77,17 @@ public class MusicUtil { } public static Uri updateStreamUri(Uri uri) { + if (uri == null) return null; + + String scheme = uri.getScheme(); + // If it is local (content:// or file://), return it IMMEDIATELY. + // This prevents the code below from appending &maxBitRate to a local path. + if (scheme != null && (scheme.equals("content") || scheme.equals("file"))) { + return uri; + } + String s = uri.toString(); + Matcher m1 = BITRATE_PATTERN.matcher(s); s = m1.replaceAll(""); Matcher m2 = FORMAT_PATTERN.matcher(s); @@ -157,7 +171,6 @@ public class MusicUtil { return Uri.parse(uri.toString()); } - public static String getReadableDurationString(Long duration, boolean millis) { long lenght = duration != null ? duration : 0; @@ -303,13 +316,17 @@ public class MusicUtil { if (network == null || networkCapabilities == null) return "raw"; + String format; if (networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) { - return Preferences.getAudioTranscodeFormatWifi(); + format = Preferences.getAudioTranscodeFormatWifi(); + Log.d(TAG, "DEBUG: Using WIFI Format: " + format); } else if (networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) { - return Preferences.getAudioTranscodeFormatMobile(); + format = Preferences.getAudioTranscodeFormatMobile(); + Log.d(TAG, "DEBUG: Using MOBILE Format: " + format); } else { - return Preferences.getAudioTranscodeFormatWifi(); + format = Preferences.getAudioTranscodeFormatWifi(); } + return format; } public static String getBitratePreferenceForDownload() { diff --git a/app/src/main/java/com/cappielloantonio/tempo/util/TranscodingMediaSource.kt b/app/src/main/java/com/cappielloantonio/tempo/util/TranscodingMediaSource.kt index 14270ff9..4213a4f7 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/util/TranscodingMediaSource.kt +++ b/app/src/main/java/com/cappielloantonio/tempo/util/TranscodingMediaSource.kt @@ -33,12 +33,18 @@ class TranscodingMediaSource( init { val extras = mediaItem.mediaMetadata.extras - if (extras != null && extras.containsKey("duration")) { + val uri = mediaItem.localConfiguration?.uri + val isLocal = uri?.scheme == "content" || uri?.scheme == "file" + + // Only apply the override if it's NOT a local file + if (!isLocal && extras != null && extras.containsKey("duration")) { val seconds = extras.getInt("duration") if (seconds > 0) { durationUs = Util.msToUs(seconds * 1000L) } } + + currentSource = progressiveMediaSourceFactory.createMediaSource(mediaItem) } override fun getMediaItem() = mediaItem