Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4cd15b4284 | ||
|
|
72d7aea6e3 | ||
|
|
9adaf8c013 | ||
|
|
661346ca3a | ||
|
|
dbd32baa12 | ||
|
|
3958cbcc1c | ||
|
|
fb568d1d74 | ||
|
|
e06a168350 | ||
|
|
b8dc985279 | ||
|
|
090701b92b | ||
|
|
7767a66fb8 | ||
|
|
d1122bef4e | ||
|
|
72d4495582 | ||
|
|
499644d041 |
22
CHANGELOG.md
22
CHANGELOG.md
@@ -1,8 +1,26 @@
|
||||
# Changelog
|
||||
|
||||
## Pending release
|
||||
## What's Changed
|
||||
## [4.11.0](https://github.com/eddyizm/tempo/releases/tag/v4.11.0) (2026-02-15)
|
||||
* fix: added dynamic application id from gradle variant by @eddyizm in https://github.com/eddyizm/tempus/pull/425
|
||||
* fix: Use Bluetooth tethering connection by @jaime-grj in https://github.com/eddyizm/tempus/pull/428
|
||||
* chore(i18n): Update Spanish translation by @jaime-grj in https://github.com/eddyizm/tempus/pull/427
|
||||
* fix: visual glitches on landscape navbar by @tvillega in https://github.com/eddyizm/tempus/pull/429
|
||||
* fix: radio playback "source error" on android auto by @dmachard in https://github.com/eddyizm/tempus/pull/426
|
||||
* fix: speed button overlaps with shuffle on landscape by @tvillega in https://github.com/eddyizm/tempus/pull/430
|
||||
* fix: local url used in share link instead of server url by @tvillega in https://github.com/eddyizm/tempus/pull/431
|
||||
* Feat :prefer downloaded files by @eddyizm in https://github.com/eddyizm/tempus/pull/433
|
||||
* fix: radio metadata displayed by @TrackArcher in https://github.com/eddyizm/tempus/pull/352
|
||||
* feat: improve playlist chooser dialog UI by @tvillega in https://github.com/eddyizm/tempus/pull/439
|
||||
|
||||
## New Contributors
|
||||
* @dmachard made their first contribution in https://github.com/eddyizm/tempus/pull/426
|
||||
* @TrackArcher made their first contribution in https://github.com/eddyizm/tempus/pull/352
|
||||
|
||||
**Full Changelog**: https://github.com/eddyizm/tempus/compare/v4.10.1...v4.11.0
|
||||
|
||||
## What's Changed
|
||||
## [4.10.1](https://github.com/eddyizm/tempo/releases/tag/v4.10.1) (2026-02-08)
|
||||
* fix: Addressing some UI/UX quirks by @tiltshiftfocus in https://github.com/eddyizm/tempus/pull/413
|
||||
* fix: keep observer until data is received on continuousPlay bug by @eddyizm in https://github.com/eddyizm/tempus/pull/421
|
||||
* fix: album art now displays on android auto by @trobinson in https://github.com/eddyizm/tempus/pull/414
|
||||
@@ -12,7 +30,7 @@
|
||||
* @tiltshiftfocus made their first contribution in https://github.com/eddyizm/tempus/pull/413
|
||||
* @trobinson made their first contribution in https://github.com/eddyizm/tempus/pull/414
|
||||
|
||||
**Full Changelog**: https://github.com/eddyizm/tempus/compare/v4.9.8...v4.10.0
|
||||
**Full Changelog**: https://github.com/eddyizm/tempus/compare/v4.9.8...v4.10.1
|
||||
|
||||
## What's Changed
|
||||
## [4.9.8](https://github.com/eddyizm/tempo/releases/tag/v4.9.8) (2026-02-02)
|
||||
|
||||
@@ -10,8 +10,8 @@ android {
|
||||
minSdkVersion 24
|
||||
targetSdk 35
|
||||
|
||||
versionCode 18
|
||||
versionName '4.10.0'
|
||||
versionCode 20
|
||||
versionName '4.11.0'
|
||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||
|
||||
javaCompileOptions {
|
||||
|
||||
@@ -98,7 +98,7 @@
|
||||
|
||||
<provider
|
||||
android:name=".provider.AlbumArtContentProvider"
|
||||
android:authorities="com.cappielloantonio.tempo.provider"
|
||||
android:authorities="${applicationId}.albumart.provider"
|
||||
android:enabled="true"
|
||||
android:exported="true"
|
||||
/>
|
||||
|
||||
@@ -55,6 +55,48 @@ public class App extends Application {
|
||||
}
|
||||
return subsonic;
|
||||
}
|
||||
|
||||
public static Subsonic getSubsonicPublicClientInstance(boolean override) {
|
||||
|
||||
/*
|
||||
If I do the shortcut that the IDE suggests:
|
||||
SubsonicPreferences preferences = getSubsonicPreferences1();
|
||||
During the chain of calls it will run the following:
|
||||
String server = Preferences.getInUseServerAddress();
|
||||
Which could return Local URL, causing issues like generating public shares with Local URL
|
||||
|
||||
To prevent this I just replicated the entire chain of functions here,
|
||||
if you need a call to Subsonic using the Server (Public) URL use this function.
|
||||
*/
|
||||
|
||||
String server = Preferences.getServer();
|
||||
String username = Preferences.getUser();
|
||||
String password = Preferences.getPassword();
|
||||
String token = Preferences.getToken();
|
||||
String salt = Preferences.getSalt();
|
||||
boolean isLowSecurity = Preferences.isLowScurity();
|
||||
|
||||
SubsonicPreferences preferences = new SubsonicPreferences();
|
||||
preferences.setServerUrl(server);
|
||||
preferences.setUsername(username);
|
||||
preferences.setAuthentication(password, token, salt, isLowSecurity);
|
||||
|
||||
if (subsonic == null || override) {
|
||||
|
||||
if (preferences.getAuthentication() != null) {
|
||||
if (preferences.getAuthentication().getPassword() != null)
|
||||
Preferences.setPassword(preferences.getAuthentication().getPassword());
|
||||
if (preferences.getAuthentication().getToken() != null)
|
||||
Preferences.setToken(preferences.getAuthentication().getToken());
|
||||
if (preferences.getAuthentication().getSalt() != null)
|
||||
Preferences.setSalt(preferences.getAuthentication().getSalt());
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
return new Subsonic(preferences);
|
||||
}
|
||||
|
||||
public static Github getGithubClientInstance() {
|
||||
if (github == null) {
|
||||
|
||||
@@ -14,6 +14,7 @@ import androidx.annotation.Nullable;
|
||||
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
import com.cappielloantonio.tempo.BuildConfig;
|
||||
import com.cappielloantonio.tempo.glide.CustomGlideRequest;
|
||||
import com.cappielloantonio.tempo.util.Preferences;
|
||||
|
||||
@@ -28,7 +29,7 @@ import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class AlbumArtContentProvider extends ContentProvider {
|
||||
public static final String AUTHORITY = "com.cappielloantonio.tempo.provider";
|
||||
public static final String AUTHORITY = BuildConfig.APPLICATION_ID + ".albumart.provider";
|
||||
public static final String ALBUM_ART = "albumArt";
|
||||
private ExecutorService executor;
|
||||
|
||||
|
||||
@@ -606,20 +606,7 @@ public class AutomotiveRepository {
|
||||
List<MediaItem> mediaItems = new ArrayList<>();
|
||||
|
||||
for (InternetRadioStation radioStation : radioStations) {
|
||||
MediaMetadata mediaMetadata = new MediaMetadata.Builder()
|
||||
.setTitle(radioStation.getName())
|
||||
.setIsBrowsable(false)
|
||||
.setIsPlayable(true)
|
||||
.setMediaType(MediaMetadata.MEDIA_TYPE_RADIO_STATION)
|
||||
.build();
|
||||
|
||||
MediaItem mediaItem = new MediaItem.Builder()
|
||||
.setMediaId(radioStation.getId())
|
||||
.setMediaMetadata(mediaMetadata)
|
||||
.setUri(radioStation.getStreamUrl())
|
||||
.build();
|
||||
|
||||
mediaItems.add(mediaItem);
|
||||
mediaItems.add(MappingUtil.mapInternetRadioStation(radioStation));
|
||||
}
|
||||
|
||||
setInternetRadioStationsMetadata(radioStations);
|
||||
|
||||
@@ -41,7 +41,7 @@ public class SharingRepository {
|
||||
public MutableLiveData<Share> createShare(String id, String description, Long expires) {
|
||||
MutableLiveData<Share> share = new MutableLiveData<>();
|
||||
|
||||
App.getSubsonicClientInstance(false)
|
||||
App.getSubsonicPublicClientInstance(false)
|
||||
.getSharingClient()
|
||||
.createShare(id, description, expires)
|
||||
.enqueue(new Callback<ApiResponse>() {
|
||||
@@ -64,7 +64,7 @@ public class SharingRepository {
|
||||
}
|
||||
|
||||
public void updateShare(String id, String description, Long expires) {
|
||||
App.getSubsonicClientInstance(false)
|
||||
App.getSubsonicPublicClientInstance(false)
|
||||
.getSharingClient()
|
||||
.updateShare(id, description, expires)
|
||||
.enqueue(new Callback<ApiResponse>() {
|
||||
|
||||
@@ -24,6 +24,9 @@ import androidx.media3.exoplayer.source.MediaSource
|
||||
import androidx.media3.exoplayer.source.ShuffleOrder.DefaultShuffleOrder
|
||||
import androidx.media3.session.*
|
||||
import androidx.media3.session.MediaSession.ControllerInfo
|
||||
import androidx.media3.extractor.metadata.icy.IcyInfo
|
||||
import androidx.media3.extractor.metadata.id3.TextInformationFrame
|
||||
import androidx.media3.extractor.metadata.vorbis.VorbisComment
|
||||
import com.cappielloantonio.tempo.R
|
||||
import com.cappielloantonio.tempo.repository.QueueRepository
|
||||
import com.cappielloantonio.tempo.ui.activity.MainActivity
|
||||
@@ -32,6 +35,12 @@ import com.cappielloantonio.tempo.widget.WidgetUpdateManager
|
||||
import com.google.common.collect.ImmutableList
|
||||
import com.google.common.util.concurrent.Futures
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.ScheduledExecutorService
|
||||
import java.util.concurrent.ScheduledFuture
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
private const val TAG = "BaseMediaService"
|
||||
|
||||
@@ -70,6 +79,13 @@ open class BaseMediaService : MediaLibraryService() {
|
||||
}
|
||||
}
|
||||
|
||||
private val radioHeaderCheckExecutor: ScheduledExecutorService = Executors.newSingleThreadScheduledExecutor()
|
||||
private var radioHeaderCheckScheduled = false
|
||||
private var radioHeaderCheckFuture: ScheduledFuture<*>? = null
|
||||
private val radioHeaderCheckRunnable = Runnable {
|
||||
checkRadioHttpHeaders()
|
||||
}
|
||||
|
||||
private val binder = LocalBinder()
|
||||
|
||||
open fun playerInitHook() {
|
||||
@@ -120,6 +136,9 @@ open class BaseMediaService : MediaLibraryService() {
|
||||
updateWidget(player)
|
||||
}
|
||||
|
||||
private var lastRadioArtist: String? = null
|
||||
private var lastRadioTitle: String? = null
|
||||
|
||||
fun initializePlayerListener(player: Player) {
|
||||
player.addListener(object : Player.Listener {
|
||||
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
|
||||
@@ -129,6 +148,16 @@ open class BaseMediaService : MediaLibraryService() {
|
||||
if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_SEEK || reason == Player.MEDIA_ITEM_TRANSITION_REASON_AUTO) {
|
||||
MediaManager.setLastPlayedTimestamp(mediaItem)
|
||||
}
|
||||
|
||||
// Restart header checks for radio streams when media item changes
|
||||
val mediaType = mediaItem.mediaMetadata.extras?.getString("type")
|
||||
if (mediaType == Constants.MEDIA_TYPE_RADIO && player.isPlaying) {
|
||||
stopRadioHeaderChecks()
|
||||
scheduleRadioHeaderChecks()
|
||||
} else if (mediaType != Constants.MEDIA_TYPE_RADIO) {
|
||||
stopRadioHeaderChecks()
|
||||
}
|
||||
|
||||
updateWidget(player)
|
||||
}
|
||||
|
||||
@@ -170,6 +199,96 @@ open class BaseMediaService : MediaLibraryService() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMetadata(metadata: Metadata) {
|
||||
// Handle streaming metadata (ICY, ID3) for radio / streaming content
|
||||
val currentItem = player.currentMediaItem ?: return
|
||||
val extras = currentItem.mediaMetadata.extras
|
||||
if (extras?.getString("type") != Constants.MEDIA_TYPE_RADIO) return
|
||||
|
||||
var artist: String? = null
|
||||
var title: String? = null
|
||||
|
||||
// Extract metadata from ICY/ID3/Vorbis
|
||||
for (i in 0 until metadata.length()) {
|
||||
when (val entry = metadata[i]) {
|
||||
is IcyInfo -> {
|
||||
entry.title?.let { icyTitle ->
|
||||
val parts = icyTitle.split(" - ", limit = 2)
|
||||
if (parts.size == 2) {
|
||||
artist = parts[0].trim().ifEmpty { null }
|
||||
title = parts[1].trim().ifEmpty { null }
|
||||
} else {
|
||||
title = icyTitle.trim().ifEmpty { null }
|
||||
}
|
||||
}
|
||||
}
|
||||
is TextInformationFrame -> {
|
||||
@Suppress("DEPRECATION")
|
||||
val value = entry.value
|
||||
when (entry.id) {
|
||||
"TPE1" -> if (!value.isNullOrBlank()) artist = value
|
||||
"TIT2" -> if (!value.isNullOrBlank()) title = value
|
||||
}
|
||||
}
|
||||
is VorbisComment -> {
|
||||
@Suppress("DEPRECATION")
|
||||
val value = entry.value
|
||||
when (entry.key) {
|
||||
"ARTIST" -> if (!value.isNullOrBlank()) artist = value
|
||||
"TITLE" -> if (!value.isNullOrBlank()) title = value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (artist.isNullOrBlank() && title.isNullOrBlank()) return
|
||||
if (artist == lastRadioArtist && title == lastRadioTitle) return // Deduplicate
|
||||
|
||||
lastRadioArtist = artist
|
||||
lastRadioTitle = title
|
||||
|
||||
// Stop HTTP header checks since we have embedded metadata
|
||||
stopRadioHeaderChecks()
|
||||
|
||||
val currentIndex = player.currentMediaItemIndex
|
||||
if (currentIndex == C.INDEX_UNSET) return
|
||||
|
||||
val metadataBuilder = currentItem.mediaMetadata.buildUpon()
|
||||
val newExtras = Bundle(extras ?: Bundle())
|
||||
|
||||
// Store individual values in extras for UI
|
||||
artist?.let { newExtras.putString("radioArtist", it) }
|
||||
title?.let { newExtras.putString("radioTitle", it) }
|
||||
|
||||
// Get station name (preserve if already set)
|
||||
val stationName = extras?.getString("stationName")
|
||||
?: currentItem.mediaMetadata.title?.toString()
|
||||
?: ""
|
||||
if (stationName.isNotBlank()) {
|
||||
newExtras.putString("stationName", stationName)
|
||||
}
|
||||
|
||||
// Format for notification/player: Title = "Artist - Song", Artist = "Station Name"
|
||||
val formattedTitle = when {
|
||||
!artist.isNullOrBlank() && !title.isNullOrBlank() -> "$artist - $title"
|
||||
!title.isNullOrBlank() -> title
|
||||
!artist.isNullOrBlank() -> artist
|
||||
else -> stationName
|
||||
}
|
||||
|
||||
metadataBuilder.setTitle(formattedTitle)
|
||||
if (stationName.isNotBlank()) {
|
||||
metadataBuilder.setArtist(stationName)
|
||||
}
|
||||
|
||||
(player as? ExoPlayer)?.let { exo ->
|
||||
exo.replaceMediaItem(currentIndex, currentItem.buildUpon()
|
||||
.setMediaMetadata(metadataBuilder.setExtras(newExtras).build())
|
||||
.build())
|
||||
updateWidget(exo)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onIsPlayingChanged(isPlaying: Boolean) {
|
||||
Log.d(TAG, "onIsPlayingChanged " + player.currentMediaItemIndex)
|
||||
if (!isPlaying) {
|
||||
@@ -182,8 +301,10 @@ open class BaseMediaService : MediaLibraryService() {
|
||||
}
|
||||
if (isPlaying) {
|
||||
scheduleWidgetUpdates()
|
||||
scheduleRadioHeaderChecks()
|
||||
} else {
|
||||
stopWidgetUpdates()
|
||||
stopRadioHeaderChecks()
|
||||
}
|
||||
updateWidget(player)
|
||||
}
|
||||
@@ -287,6 +408,8 @@ open class BaseMediaService : MediaLibraryService() {
|
||||
releaseNetworkCallback()
|
||||
equalizerManager.release()
|
||||
stopWidgetUpdates()
|
||||
stopRadioHeaderChecks()
|
||||
radioHeaderCheckExecutor.shutdown()
|
||||
releasePlayers()
|
||||
mediaLibrarySession.release()
|
||||
super.onDestroy()
|
||||
@@ -405,6 +528,148 @@ open class BaseMediaService : MediaLibraryService() {
|
||||
widgetUpdateScheduled = false
|
||||
}
|
||||
|
||||
private fun scheduleRadioHeaderChecks() {
|
||||
val player = mediaLibrarySession.player
|
||||
val currentItem = player.currentMediaItem ?: return
|
||||
val mediaType = currentItem.mediaMetadata.extras?.getString("type")
|
||||
if (mediaType != Constants.MEDIA_TYPE_RADIO) return
|
||||
|
||||
if (radioHeaderCheckScheduled) return
|
||||
|
||||
// Check immediately, then periodically
|
||||
checkRadioHttpHeaders()
|
||||
radioHeaderCheckFuture = radioHeaderCheckExecutor.scheduleWithFixedDelay(
|
||||
radioHeaderCheckRunnable,
|
||||
RADIO_HEADER_CHECK_INTERVAL_SECONDS,
|
||||
RADIO_HEADER_CHECK_INTERVAL_SECONDS,
|
||||
TimeUnit.SECONDS
|
||||
)
|
||||
radioHeaderCheckScheduled = true
|
||||
}
|
||||
|
||||
private fun stopRadioHeaderChecks() {
|
||||
if (!radioHeaderCheckScheduled) return
|
||||
radioHeaderCheckFuture?.cancel(false)
|
||||
radioHeaderCheckFuture = null
|
||||
radioHeaderCheckScheduled = false
|
||||
}
|
||||
|
||||
private fun checkRadioHttpHeaders() {
|
||||
val player = mediaLibrarySession.player
|
||||
val currentItem = player.currentMediaItem ?: return
|
||||
val extras = currentItem.mediaMetadata.extras
|
||||
val mediaType = extras?.getString("type")
|
||||
if (mediaType != Constants.MEDIA_TYPE_RADIO) return
|
||||
|
||||
// Skip if we already have embedded metadata (ICY/ID3) - HTTP headers are only fallback
|
||||
val hasEmbeddedMetadata = !currentItem.mediaMetadata.artist.isNullOrBlank() ||
|
||||
!currentItem.mediaMetadata.title.isNullOrBlank() ||
|
||||
(extras != null && !extras.getString("radioArtist").isNullOrBlank()) ||
|
||||
(extras != null && !extras.getString("radioTitle").isNullOrBlank())
|
||||
if (hasEmbeddedMetadata) return
|
||||
|
||||
val streamUrl = extras?.getString("uri") ?: currentItem.requestMetadata.mediaUri?.toString()
|
||||
if (streamUrl.isNullOrBlank()) return
|
||||
|
||||
try {
|
||||
val url = URL(streamUrl)
|
||||
val connection = url.openConnection() as? HttpURLConnection ?: return
|
||||
|
||||
// Only try HEAD request (lightweight) - skip GET fallback as it's unreliable
|
||||
connection.requestMethod = "HEAD"
|
||||
connection.setRequestProperty("Icy-MetaData", "1")
|
||||
connection.setRequestProperty("User-Agent", "Tempus/1.0")
|
||||
connection.connectTimeout = 3000 // Reduced timeout
|
||||
connection.readTimeout = 3000
|
||||
|
||||
connection.connect()
|
||||
|
||||
if (connection.responseCode >= 400) {
|
||||
connection.disconnect()
|
||||
return
|
||||
}
|
||||
|
||||
// Check for metadata in HTTP headers
|
||||
val streamTitle = connection.getHeaderField("icy-name")
|
||||
?: connection.getHeaderField("StreamTitle")
|
||||
?: connection.getHeaderField("stream-title")
|
||||
|
||||
connection.disconnect()
|
||||
|
||||
if (!streamTitle.isNullOrBlank()) {
|
||||
processStreamTitle(streamTitle, player)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// Silently fail - this is a fallback mechanism, ICY metadata is primary
|
||||
}
|
||||
}
|
||||
|
||||
private fun processStreamTitle(streamTitle: String, player: Player) {
|
||||
// Parse "Artist - Title" format
|
||||
val parts = streamTitle.split(" - ", limit = 2)
|
||||
val artist = if (parts.size == 2) parts[0].trim().ifEmpty { null } else null
|
||||
val title = if (parts.size == 2) parts[1].trim().ifEmpty { null } else streamTitle.trim().ifEmpty { null }
|
||||
|
||||
if (artist.isNullOrBlank() && title.isNullOrBlank()) return
|
||||
if (artist == lastRadioArtist && title == lastRadioTitle) return // Deduplicate
|
||||
|
||||
lastRadioArtist = artist
|
||||
lastRadioTitle = title
|
||||
|
||||
// Update on main thread
|
||||
widgetUpdateHandler.post {
|
||||
val currentItemNow = player.currentMediaItem ?: return@post
|
||||
val currentIndex = player.currentMediaItemIndex
|
||||
if (currentIndex == C.INDEX_UNSET) return@post
|
||||
|
||||
val currentExtras = currentItemNow.mediaMetadata.extras
|
||||
if (currentExtras?.getString("type") != Constants.MEDIA_TYPE_RADIO) return@post
|
||||
|
||||
// Double-check we still don't have embedded metadata (might have arrived since check)
|
||||
val hasEmbeddedMetadata = !currentItemNow.mediaMetadata.artist.isNullOrBlank() ||
|
||||
!currentItemNow.mediaMetadata.title.isNullOrBlank() ||
|
||||
(currentExtras != null && !currentExtras.getString("radioArtist").isNullOrBlank()) ||
|
||||
(currentExtras != null && !currentExtras.getString("radioTitle").isNullOrBlank())
|
||||
if (hasEmbeddedMetadata) return@post
|
||||
|
||||
val metadataBuilder = currentItemNow.mediaMetadata.buildUpon()
|
||||
val newExtras = Bundle(currentExtras ?: Bundle())
|
||||
|
||||
// Store individual values in extras for UI
|
||||
artist?.let { newExtras.putString("radioArtist", it) }
|
||||
title?.let { newExtras.putString("radioTitle", it) }
|
||||
|
||||
// Get station name (preserve if already set)
|
||||
val stationName = currentExtras?.getString("stationName")
|
||||
?: currentItemNow.mediaMetadata.title?.toString()
|
||||
?: ""
|
||||
if (stationName.isNotBlank()) {
|
||||
newExtras.putString("stationName", stationName)
|
||||
}
|
||||
|
||||
// Format for notification/player: Title = "Artist - Song", Artist = "Station Name"
|
||||
val formattedTitle = when {
|
||||
!artist.isNullOrBlank() && !title.isNullOrBlank() -> "$artist - $title"
|
||||
!title.isNullOrBlank() -> title
|
||||
!artist.isNullOrBlank() -> artist
|
||||
else -> stationName
|
||||
}
|
||||
|
||||
metadataBuilder.setTitle(formattedTitle)
|
||||
if (stationName.isNotBlank()) {
|
||||
metadataBuilder.setArtist(stationName)
|
||||
}
|
||||
metadataBuilder.setExtras(newExtras)
|
||||
|
||||
(player as? ExoPlayer)?.let { exo ->
|
||||
exo.replaceMediaItem(currentIndex, currentItemNow.buildUpon()
|
||||
.setMediaMetadata(metadataBuilder.build())
|
||||
.build())
|
||||
updateWidget(exo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun attachEqualizerIfPossible(audioSessionId: Int): Boolean {
|
||||
if (audioSessionId == 0 || audioSessionId == -1) return false
|
||||
val attached = equalizerManager.attachToSession(audioSessionId)
|
||||
@@ -595,4 +860,5 @@ open class BaseMediaService : MediaLibraryService() {
|
||||
}
|
||||
|
||||
private const val WIDGET_UPDATE_INTERVAL_MS = 1000L
|
||||
private const val RADIO_HEADER_CHECK_INTERVAL_SECONDS = 30L // Reduced frequency - only fallback when ICY fails
|
||||
|
||||
|
||||
@@ -62,7 +62,8 @@ public class CacheUtil {
|
||||
|
||||
boolean hasAppropriateTransport = capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)
|
||||
|| capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
|
||||
|| capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET);
|
||||
|| capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)
|
||||
|| capabilities.hasTransport(NetworkCapabilities.TRANSPORT_BLUETOOTH);
|
||||
if (!hasAppropriateTransport) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import android.view.View;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
@@ -20,41 +19,30 @@ import com.cappielloantonio.tempo.util.Constants;
|
||||
import com.cappielloantonio.tempo.viewmodel.PlaylistChooserViewModel;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
public class PlaylistChooserDialog extends DialogFragment implements ClickCallback {
|
||||
private DialogPlaylistChooserBinding bind;
|
||||
private PlaylistChooserViewModel playlistChooserViewModel;
|
||||
|
||||
private PlaylistDialogHorizontalAdapter playlistDialogHorizontalAdapter;
|
||||
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
DialogPlaylistChooserBinding.inflate(getLayoutInflater());
|
||||
bind = DialogPlaylistChooserBinding.inflate(getLayoutInflater());
|
||||
|
||||
playlistChooserViewModel = new ViewModelProvider(requireActivity()).get(PlaylistChooserViewModel.class);
|
||||
|
||||
String[] playlistVisibilityChoice = {
|
||||
getString(R.string.playlist_chooser_dialog_visibility_public),
|
||||
getString(R.string.playlist_chooser_dialog_visibility_private)
|
||||
};
|
||||
bind.playlistDialogChooserVisibilitySwitch.setOnCheckedChangeListener(
|
||||
(buttonView,
|
||||
isChecked) -> playlistChooserViewModel.setIsPlaylistPublic(isChecked)
|
||||
);
|
||||
bind.playlistChooserDialogCreateButton.setOnClickListener(v -> launchPlaylistEditor());
|
||||
bind.playlistChooserDialogCancelButton.setOnClickListener(v -> dismiss());
|
||||
|
||||
return new MaterialAlertDialogBuilder(getActivity())
|
||||
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireContext())
|
||||
.setView(bind.getRoot())
|
||||
.setTitle(R.string.playlist_chooser_dialog_title)
|
||||
.setSingleChoiceItems(
|
||||
playlistVisibilityChoice,
|
||||
0,
|
||||
(dialog, which) -> {
|
||||
boolean isPublic = (which == 0);
|
||||
playlistChooserViewModel.setIsPlaylistPublic(isPublic);
|
||||
})
|
||||
.setNeutralButton(R.string.playlist_chooser_dialog_neutral_button, (dialog, id) -> { })
|
||||
.setNegativeButton(R.string.playlist_chooser_dialog_negative_button, (dialog, id) -> dialog.cancel())
|
||||
.create();
|
||||
.setTitle(R.string.playlist_chooser_dialog_title);
|
||||
return builder.create();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -69,25 +57,26 @@ public class PlaylistChooserDialog extends DialogFragment implements ClickCallba
|
||||
|
||||
initPlaylistView();
|
||||
setSongInfo();
|
||||
setButtonAction();
|
||||
}
|
||||
|
||||
private void setSongInfo() {
|
||||
playlistChooserViewModel.setSongsToAdd(requireArguments().getParcelableArrayList(Constants.TRACKS_OBJECT));
|
||||
}
|
||||
|
||||
private void setButtonAction() {
|
||||
androidx.appcompat.app.AlertDialog alertDialog = (androidx.appcompat.app.AlertDialog) Objects.requireNonNull(getDialog());
|
||||
alertDialog.getButton(androidx.appcompat.app.AlertDialog.BUTTON_NEUTRAL).setOnClickListener(v -> {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putParcelableArrayList(Constants.TRACKS_OBJECT, playlistChooserViewModel.getSongsToAdd());
|
||||
private void launchPlaylistEditor() {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putParcelableArrayList(
|
||||
Constants.TRACKS_OBJECT,
|
||||
playlistChooserViewModel.getSongsToAdd()
|
||||
);
|
||||
|
||||
PlaylistEditorDialog dialog = new PlaylistEditorDialog(null);
|
||||
dialog.setArguments(bundle);
|
||||
dialog.show(requireActivity().getSupportFragmentManager(), null);
|
||||
PlaylistEditorDialog editorDialog = new PlaylistEditorDialog(null);
|
||||
editorDialog.setArguments(bundle);
|
||||
editorDialog.show(
|
||||
requireActivity().getSupportFragmentManager(),
|
||||
null);
|
||||
|
||||
Objects.requireNonNull(getDialog()).dismiss();
|
||||
});
|
||||
dismiss();
|
||||
}
|
||||
|
||||
private void initPlaylistView() {
|
||||
|
||||
@@ -61,13 +61,47 @@ public class TrackInfoDialog extends DialogFragment {
|
||||
private void setTrackInfo() {
|
||||
genreLink = null;
|
||||
yearLink = null;
|
||||
bind.trakTitleInfoTextView.setText(mediaMetadata.title);
|
||||
bind.trakArtistInfoTextView.setText(
|
||||
mediaMetadata.artist != null
|
||||
? mediaMetadata.artist
|
||||
: mediaMetadata.extras != null && Objects.equals(mediaMetadata.extras.getString("type"), Constants.MEDIA_TYPE_RADIO)
|
||||
? mediaMetadata.extras.getString("uri", getString(R.string.label_placeholder))
|
||||
: "");
|
||||
|
||||
String type = mediaMetadata.extras != null ? mediaMetadata.extras.getString("type") : null;
|
||||
boolean isRadio = Objects.equals(type, Constants.MEDIA_TYPE_RADIO);
|
||||
|
||||
if (isRadio) {
|
||||
// For radio: always read from extras first (radioArtist, radioTitle, stationName)
|
||||
// MediaMetadata.title/artist are formatted for notification
|
||||
String stationName = mediaMetadata.extras != null
|
||||
? mediaMetadata.extras.getString("stationName",
|
||||
mediaMetadata.artist != null ? String.valueOf(mediaMetadata.artist) : "")
|
||||
: mediaMetadata.artist != null ? String.valueOf(mediaMetadata.artist) : "";
|
||||
|
||||
String artist = mediaMetadata.extras != null
|
||||
? mediaMetadata.extras.getString("radioArtist", "")
|
||||
: "";
|
||||
|
||||
String title = mediaMetadata.extras != null
|
||||
? mediaMetadata.extras.getString("radioTitle", "")
|
||||
: "";
|
||||
|
||||
// Format: "Artist - Song" or fallback to title or station name
|
||||
String mainTitle;
|
||||
if (!android.text.TextUtils.isEmpty(artist) && !android.text.TextUtils.isEmpty(title)) {
|
||||
mainTitle = artist + " - " + title;
|
||||
} else if (!android.text.TextUtils.isEmpty(title)) {
|
||||
mainTitle = title;
|
||||
} else if (!android.text.TextUtils.isEmpty(artist)) {
|
||||
mainTitle = artist;
|
||||
} else {
|
||||
mainTitle = stationName;
|
||||
}
|
||||
|
||||
bind.trakTitleInfoTextView.setText(mainTitle);
|
||||
bind.trakArtistInfoTextView.setText(stationName);
|
||||
} else {
|
||||
bind.trakTitleInfoTextView.setText(mediaMetadata.title);
|
||||
bind.trakArtistInfoTextView.setText(
|
||||
mediaMetadata.artist != null
|
||||
? mediaMetadata.artist
|
||||
: "");
|
||||
}
|
||||
|
||||
if (mediaMetadata.extras != null) {
|
||||
songLink = AssetLinkUtil.buildAssetLink(AssetLinkUtil.TYPE_SONG, mediaMetadata.extras.getString("id"));
|
||||
@@ -90,6 +124,27 @@ public class TrackInfoDialog extends DialogFragment {
|
||||
String artistValue = mediaMetadata.extras.getString("artist", getString(R.string.label_placeholder));
|
||||
String genreValue = mediaMetadata.extras.getString("genre", getString(R.string.label_placeholder));
|
||||
int yearValue = mediaMetadata.extras.getInt("year", 0);
|
||||
|
||||
// Handle radio-specific metadata
|
||||
if (isRadio) {
|
||||
String stationName = mediaMetadata.extras.getString("stationName", getString(R.string.label_placeholder));
|
||||
String radioArtist = mediaMetadata.extras.getString("radioArtist", "");
|
||||
String radioTitle = mediaMetadata.extras.getString("radioTitle", "");
|
||||
|
||||
// Show station name in station section
|
||||
bind.stationInfoSector.setVisibility(android.view.View.VISIBLE);
|
||||
bind.stationValueSector.setText(stationName);
|
||||
|
||||
// Use radio metadata for title/artist if available
|
||||
if (!android.text.TextUtils.isEmpty(radioTitle)) {
|
||||
titleValue = radioTitle;
|
||||
}
|
||||
if (!android.text.TextUtils.isEmpty(radioArtist)) {
|
||||
artistValue = radioArtist;
|
||||
}
|
||||
} else {
|
||||
bind.stationInfoSector.setVisibility(android.view.View.GONE);
|
||||
}
|
||||
|
||||
if (genreLink == null && genreValue != null && !genreValue.isEmpty() && !getString(R.string.label_placeholder).contentEquals(genreValue)) {
|
||||
genreLink = AssetLinkUtil.buildAssetLink(AssetLinkUtil.TYPE_GENRE, genreValue);
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.cappielloantonio.tempo.ui.fragment;
|
||||
import android.content.ComponentName;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.text.TextUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
@@ -173,25 +174,54 @@ public class PlayerBottomSheetFragment extends Fragment {
|
||||
playerBottomSheetViewModel.setLiveArtist(getViewLifecycleOwner(), mediaMetadata.extras.getString("type"), mediaMetadata.extras.getString("artistId"));
|
||||
playerBottomSheetViewModel.setLiveDescription(mediaMetadata.extras.getString("description", null));
|
||||
|
||||
bind.playerHeaderLayout.playerHeaderMediaTitleLabel.setText(mediaMetadata.extras.getString("title"));
|
||||
bind.playerHeaderLayout.playerHeaderMediaArtistLabel.setText(
|
||||
mediaMetadata.artist != null
|
||||
? mediaMetadata.artist
|
||||
: Objects.equals(mediaMetadata.extras.getString("type"), Constants.MEDIA_TYPE_RADIO)
|
||||
? mediaMetadata.extras.getString("uri", getString(R.string.label_placeholder))
|
||||
: "");
|
||||
String type = mediaMetadata.extras.getString("type");
|
||||
|
||||
if (Objects.equals(type, Constants.MEDIA_TYPE_RADIO)) {
|
||||
// For radio: keep header consistent with full player
|
||||
String stationName = mediaMetadata.extras.getString(
|
||||
"stationName",
|
||||
mediaMetadata.artist != null ? String.valueOf(mediaMetadata.artist) : ""
|
||||
);
|
||||
|
||||
String artist = mediaMetadata.extras.getString("radioArtist", "");
|
||||
String title = mediaMetadata.extras.getString("radioTitle", "");
|
||||
|
||||
String mainTitle;
|
||||
if (!TextUtils.isEmpty(artist) && !TextUtils.isEmpty(title)) {
|
||||
mainTitle = artist + " - " + title;
|
||||
} else if (!TextUtils.isEmpty(title)) {
|
||||
mainTitle = title;
|
||||
} else if (!TextUtils.isEmpty(artist)) {
|
||||
mainTitle = artist;
|
||||
} else {
|
||||
mainTitle = stationName;
|
||||
}
|
||||
|
||||
bind.playerHeaderLayout.playerHeaderMediaTitleLabel.setText(mainTitle);
|
||||
bind.playerHeaderLayout.playerHeaderMediaArtistLabel.setText(stationName);
|
||||
|
||||
bind.playerHeaderLayout.playerHeaderMediaTitleLabel.setVisibility(!TextUtils.isEmpty(mainTitle) ? View.VISIBLE : View.GONE);
|
||||
bind.playerHeaderLayout.playerHeaderMediaArtistLabel.setVisibility(!TextUtils.isEmpty(stationName) ? View.VISIBLE : View.GONE);
|
||||
} else {
|
||||
// Default (music, podcast, etc.)
|
||||
bind.playerHeaderLayout.playerHeaderMediaTitleLabel.setText(mediaMetadata.extras.getString("title"));
|
||||
bind.playerHeaderLayout.playerHeaderMediaArtistLabel.setText(
|
||||
mediaMetadata.artist != null
|
||||
? mediaMetadata.artist
|
||||
: ""
|
||||
);
|
||||
|
||||
bind.playerHeaderLayout.playerHeaderMediaTitleLabel.setVisibility(mediaMetadata.extras.getString("title") != null && !Objects.equals(mediaMetadata.extras.getString("title"), "") ? View.VISIBLE : View.GONE);
|
||||
bind.playerHeaderLayout.playerHeaderMediaArtistLabel.setVisibility(
|
||||
mediaMetadata.extras.getString("artist") != null && !Objects.equals(mediaMetadata.extras.getString("artist"), "")
|
||||
? View.VISIBLE
|
||||
: View.GONE);
|
||||
}
|
||||
|
||||
CustomGlideRequest.Builder
|
||||
.from(requireContext(), mediaMetadata.extras.getString("coverArtId"), CustomGlideRequest.ResourceType.Song)
|
||||
.build()
|
||||
.into(bind.playerHeaderLayout.playerHeaderMediaCoverImage);
|
||||
|
||||
bind.playerHeaderLayout.playerHeaderMediaTitleLabel.setVisibility(mediaMetadata.extras.getString("title") != null && !Objects.equals(mediaMetadata.extras.getString("title"), "") ? View.VISIBLE : View.GONE);
|
||||
bind.playerHeaderLayout.playerHeaderMediaArtistLabel.setVisibility(
|
||||
(mediaMetadata.extras.getString("artist") != null && !Objects.equals(mediaMetadata.extras.getString("artist"), ""))
|
||||
|| (Objects.equals(mediaMetadata.extras.getString("type"), Constants.MEDIA_TYPE_RADIO) && mediaMetadata.extras.getString("uri") != null)
|
||||
? View.VISIBLE
|
||||
: View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -214,12 +215,53 @@ public class PlayerControllerFragment extends Fragment {
|
||||
}
|
||||
|
||||
private void setMetadata(MediaMetadata mediaMetadata) {
|
||||
String type = mediaMetadata.extras != null ? mediaMetadata.extras.getString("type") : null;
|
||||
|
||||
if (Objects.equals(type, Constants.MEDIA_TYPE_RADIO)) {
|
||||
// For radio: always read from extras first (radioArtist, radioTitle, stationName)
|
||||
// MediaMetadata.title/artist are formatted for notification
|
||||
String stationName = mediaMetadata.extras != null
|
||||
? mediaMetadata.extras.getString("stationName",
|
||||
mediaMetadata.artist != null ? String.valueOf(mediaMetadata.artist) : "")
|
||||
: mediaMetadata.artist != null ? String.valueOf(mediaMetadata.artist) : "";
|
||||
|
||||
String artist = mediaMetadata.extras != null
|
||||
? mediaMetadata.extras.getString("radioArtist", "")
|
||||
: "";
|
||||
|
||||
String title = mediaMetadata.extras != null
|
||||
? mediaMetadata.extras.getString("radioTitle", "")
|
||||
: "";
|
||||
|
||||
// Format: "Artist - Song" or fallback to title or station name
|
||||
String mainTitle;
|
||||
if (!TextUtils.isEmpty(artist) && !TextUtils.isEmpty(title)) {
|
||||
mainTitle = artist + " - " + title;
|
||||
} else if (!TextUtils.isEmpty(title)) {
|
||||
mainTitle = title;
|
||||
} else if (!TextUtils.isEmpty(artist)) {
|
||||
mainTitle = artist;
|
||||
} else {
|
||||
mainTitle = stationName;
|
||||
}
|
||||
|
||||
playerMediaTitleLabel.setText(mainTitle);
|
||||
playerArtistNameLabel.setText(stationName);
|
||||
|
||||
playerMediaTitleLabel.setSelected(true);
|
||||
playerArtistNameLabel.setSelected(true);
|
||||
|
||||
playerMediaTitleLabel.setVisibility(!TextUtils.isEmpty(mainTitle) ? View.VISIBLE : View.GONE);
|
||||
playerArtistNameLabel.setVisibility(!TextUtils.isEmpty(stationName) ? View.VISIBLE : View.GONE);
|
||||
|
||||
updateAssetLinkChips(mediaMetadata);
|
||||
return;
|
||||
}
|
||||
|
||||
playerMediaTitleLabel.setText(String.valueOf(mediaMetadata.title));
|
||||
playerArtistNameLabel.setText(
|
||||
mediaMetadata.artist != null
|
||||
? String.valueOf(mediaMetadata.artist)
|
||||
: mediaMetadata.extras != null && Objects.equals(mediaMetadata.extras.getString("type"), Constants.MEDIA_TYPE_RADIO)
|
||||
? mediaMetadata.extras.getString("uri", getString(R.string.label_placeholder))
|
||||
: "");
|
||||
|
||||
playerMediaTitleLabel.setSelected(true);
|
||||
@@ -236,43 +278,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<String> 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<String> 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;
|
||||
|
||||
@@ -29,6 +29,8 @@ import java.net.CookieHandler;
|
||||
import java.net.CookieManager;
|
||||
import java.net.CookiePolicy;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
@UnstableApi
|
||||
@@ -78,12 +80,33 @@ public final class DownloadUtil {
|
||||
return httpDataSourceFactory;
|
||||
}
|
||||
|
||||
public static synchronized DataSource.Factory getHttpDataSourceFactoryForRadio() {
|
||||
CookieManager cookieManager = new CookieManager();
|
||||
cookieManager.setCookiePolicy(CookiePolicy.ACCEPT_ORIGINAL_SERVER);
|
||||
CookieHandler.setDefault(cookieManager);
|
||||
|
||||
// Create a factory with ICY metadata support for radio streams
|
||||
Map<String, String> defaultRequestProperties = new HashMap<>();
|
||||
defaultRequestProperties.put("Icy-MetaData", "1");
|
||||
defaultRequestProperties.put("User-Agent", "Tempus/1.0");
|
||||
|
||||
return new DefaultHttpDataSource
|
||||
.Factory()
|
||||
.setAllowCrossProtocolRedirects(true)
|
||||
.setDefaultRequestProperties(defaultRequestProperties);
|
||||
}
|
||||
|
||||
public static synchronized DataSource.Factory getUpstreamDataSourceFactory(Context context) {
|
||||
DefaultDataSource.Factory upstreamFactory = new DefaultDataSource.Factory(context, getHttpDataSourceFactory());
|
||||
dataSourceFactory = buildReadOnlyCacheDataSource(upstreamFactory, getDownloadCache(context));
|
||||
return dataSourceFactory;
|
||||
}
|
||||
|
||||
public static synchronized DataSource.Factory getUpstreamDataSourceFactoryForRadio(Context context) {
|
||||
DefaultDataSource.Factory upstreamFactory = new DefaultDataSource.Factory(context, getHttpDataSourceFactoryForRadio());
|
||||
return buildReadOnlyCacheDataSource(upstreamFactory, getDownloadCache(context));
|
||||
}
|
||||
|
||||
public static synchronized DataSource.Factory getCacheDataSourceFactory(Context context) {
|
||||
CacheDataSource.Factory streamCacheFactory = new CacheDataSource.Factory()
|
||||
.setCache(getStreamingCache(context))
|
||||
|
||||
@@ -20,10 +20,15 @@ class DynamicMediaSourceFactory(
|
||||
) : MediaSource.Factory {
|
||||
|
||||
override fun createMediaSource(mediaItem: MediaItem): MediaSource {
|
||||
val mediaType: String? = mediaItem.mediaMetadata.extras?.getString("type", "")
|
||||
// Detect radio streams in a backwards-compatible way.
|
||||
// Older Tempus versions tagged radio items via MediaMetadata extras
|
||||
// (`type == MEDIA_TYPE_RADIO`), while newer upstream changes use an
|
||||
// "ir-" mediaId prefix. Support BOTH so radio works after rebases.
|
||||
val mediaType = mediaItem.mediaMetadata.extras?.getString("type", "")
|
||||
val isRadio = mediaType == Constants.MEDIA_TYPE_RADIO || mediaItem.mediaId.startsWith("ir-")
|
||||
|
||||
val streamingCacheSize = Preferences.getStreamingCacheSize()
|
||||
val bypassCache = mediaType == Constants.MEDIA_TYPE_RADIO
|
||||
val bypassCache = isRadio
|
||||
|
||||
val useUpstream = when {
|
||||
streamingCacheSize.toInt() == 0 -> true
|
||||
@@ -32,7 +37,10 @@ class DynamicMediaSourceFactory(
|
||||
else -> true
|
||||
}
|
||||
|
||||
val dataSourceFactory: DataSource.Factory = if (useUpstream) {
|
||||
val dataSourceFactory: DataSource.Factory = if (bypassCache) {
|
||||
// For radio streams, use a DataSourceFactory with ICY metadata support
|
||||
DownloadUtil.getUpstreamDataSourceFactoryForRadio(context)
|
||||
} else if (useUpstream) {
|
||||
DownloadUtil.getUpstreamDataSourceFactory(context)
|
||||
} else {
|
||||
DownloadUtil.getCacheDataSourceFactory(context)
|
||||
|
||||
@@ -211,6 +211,7 @@ public class MappingUtil {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putString("id", internetRadioStation.getId());
|
||||
bundle.putString("title", internetRadioStation.getName());
|
||||
bundle.putString("stationName", internetRadioStation.getName());
|
||||
bundle.putString("uri", uri.toString());
|
||||
bundle.putString("type", Constants.MEDIA_TYPE_RADIO);
|
||||
|
||||
@@ -219,6 +220,7 @@ public class MappingUtil {
|
||||
.setMediaMetadata(
|
||||
new MediaMetadata.Builder()
|
||||
.setTitle(internetRadioStation.getName())
|
||||
.setMediaType(MediaMetadata.MEDIA_TYPE_RADIO_STATION)
|
||||
.setExtras(bundle)
|
||||
.setIsBrowsable(false)
|
||||
.setIsPlayable(true)
|
||||
@@ -288,13 +290,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) {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -18,18 +18,17 @@
|
||||
android:orientation="horizontal">
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="75dp"
|
||||
android:layout_width="55dp"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<com.google.android.material.bottomnavigation.BottomNavigationView
|
||||
android:id="@+id/bottom_navigation"
|
||||
android:layout_width="250dp"
|
||||
android:layout_height="75dp"
|
||||
android:rotation="90"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="55dp"
|
||||
android:layout_gravity="center"
|
||||
android:paddingStart="0dp"
|
||||
android:paddingEnd="0dp"
|
||||
android:visibility="gone"
|
||||
android:paddingEnd="68dp"
|
||||
android:rotation="90"
|
||||
app:menu="@menu/bottom_nav_menu" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/now_playing_media_controller_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
@@ -62,6 +63,20 @@
|
||||
app:srcCompat="@drawable/ic_info_stream"
|
||||
app:tint="?attr/colorOnPrimaryContainer" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/player_playback_speed_button"
|
||||
style="@style/Widget.Material3.Button.TextButton"
|
||||
android:layout_width="64dp"
|
||||
android:layout_height="64dp"
|
||||
android:insetLeft="0dp"
|
||||
android:insetTop="0dp"
|
||||
android:insetRight="0dp"
|
||||
android:insetBottom="0dp"
|
||||
app:cornerRadius="30dp"
|
||||
app:tint="?attr/colorOnPrimaryContainer"
|
||||
tools:layout_editor_absoluteX="36dp"
|
||||
tools:layout_editor_absoluteY="2dp" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<androidx.viewpager2.widget.ViewPager2
|
||||
@@ -244,23 +259,6 @@
|
||||
app:layout_constraintStart_toEndOf="@+id/placeholder_view_middle_right"
|
||||
app:layout_constraintTop_toTopOf="@+id/placeholder_view_middle_right" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/player_playback_speed_button"
|
||||
style="@style/Widget.Material3.Button.TextButton"
|
||||
android:layout_width="64dp"
|
||||
android:layout_height="64dp"
|
||||
android:layout_marginStart="24dp"
|
||||
android:insetLeft="0dp"
|
||||
android:insetTop="0dp"
|
||||
android:insetRight="0dp"
|
||||
android:insetBottom="0dp"
|
||||
app:cornerRadius="30dp"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/placeholder_view_middle_left"
|
||||
app:layout_constraintEnd_toStartOf="@+id/placeholder_view_middle_left"
|
||||
app:layout_constraintStart_toEndOf="@+id/vertical_guideline"
|
||||
app:layout_constraintTop_toTopOf="@+id/placeholder_view_middle_left"
|
||||
app:tint="?attr/colorOnPrimaryContainer" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/exo_shuffle"
|
||||
android:layout_width="32dp"
|
||||
|
||||
@@ -3,6 +3,26 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.android.material.materialswitch.MaterialSwitch
|
||||
android:id="@+id/playlist_dialog_chooser_visibility_switch"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:paddingStart="30dp"
|
||||
android:paddingEnd="30dp"
|
||||
android:checked="false"
|
||||
android:showText="false"
|
||||
android:text="@string/playlist_chooser_dialog_visibility_switch_label" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/playlist_dialog_chooser_visibility_summary"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="30dp"
|
||||
android:paddingEnd="30dp"
|
||||
android:text="@string/playlist_chooser_dialog_visibility_summary"
|
||||
android:layout_marginTop="8dp"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/no_playlists_created_text_view"
|
||||
style="@style/TitleMedium"
|
||||
@@ -23,4 +43,35 @@
|
||||
android:layout_weight="1"
|
||||
android:layout_marginTop="8dp"
|
||||
android:clipToPadding="false" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/button_bar"
|
||||
style="?android:attr/buttonBarStyle"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="bottom|center_horizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:weightSum="2"
|
||||
android:layout_marginTop="16dp">
|
||||
|
||||
<Button
|
||||
android:id="@+id/playlist_chooser_dialog_create_button"
|
||||
style="?android:attr/buttonBarButtonStyle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:layout_gravity="start"
|
||||
android:text="@string/playlist_chooser_dialog_create_button" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/playlist_chooser_dialog_cancel_button"
|
||||
style="?android:attr/buttonBarButtonStyle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_gravity="end"
|
||||
android:text="@string/playlist_chooser_dialog_cancel_button"/>
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
@@ -131,6 +131,33 @@
|
||||
android:text="@string/label_placeholder" />
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
style="@style/Divider"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginVertical="8dp" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/station_info_sector"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/station_key_sector"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="4"
|
||||
android:paddingEnd="8dp"
|
||||
android:text="@string/track_info_station" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/station_value_sector"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="7"
|
||||
android:text="@string/label_placeholder" />
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
style="@style/Divider"
|
||||
android:layout_gravity="center_vertical"
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<menu
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<item
|
||||
android:id="@+id/homeFragment"
|
||||
android:icon="@drawable/ic_home_land"
|
||||
android:title="@string/menu_home_label" />
|
||||
android:icon="@drawable/ic_home_land" />
|
||||
<item
|
||||
android:id="@+id/libraryFragment"
|
||||
android:icon="@drawable/ic_graphic_eq_land"
|
||||
android:title="@string/menu_library_label" />
|
||||
android:icon="@drawable/ic_graphic_eq_land" />
|
||||
<item
|
||||
android:id="@+id/downloadFragment"
|
||||
android:icon="@drawable/ic_play_for_work_land"
|
||||
android:title="@string/menu_download_label" />
|
||||
android:icon="@drawable/ic_play_for_work_land" />
|
||||
</menu>
|
||||
@@ -4,6 +4,7 @@
|
||||
android:id="@+id/homeFragment"
|
||||
android:icon="@drawable/ic_home"
|
||||
android:title="@string/menu_home_label" />
|
||||
|
||||
<item
|
||||
android:id="@+id/libraryFragment"
|
||||
android:icon="@drawable/ic_graphic_eq"
|
||||
|
||||
@@ -224,8 +224,8 @@
|
||||
<string name="playlist_catalogue_title">Catàleg de llistes de reproducció</string>
|
||||
<string name="playlist_catalogue_title_expanded">Exploració de llistes de reproducció</string>
|
||||
<string name="playlist_chooser_dialog_empty">No s\'ha creat cap llista de reproducció</string>
|
||||
<string name="playlist_chooser_dialog_negative_button">Cancel·la</string>
|
||||
<string name="playlist_chooser_dialog_neutral_button">Crea</string>
|
||||
<string name="playlist_chooser_dialog_cancel_button">Cancel·la</string>
|
||||
<string name="playlist_chooser_dialog_create_button">Crea</string>
|
||||
<string name="playlist_chooser_dialog_title">Addició a una llista de reproducció</string>
|
||||
<string name="playlist_chooser_dialog_toast_add_success">S\'han afegit les cançons a la llista de reproducció</string>
|
||||
<string name="playlist_chooser_dialog_toast_add_failure">No s\'han pogut afegir les cançons a la llista de reproducció</string>
|
||||
|
||||
@@ -188,8 +188,8 @@
|
||||
<string name="playlist_catalogue_title">Playlisten</string>
|
||||
<string name="playlist_catalogue_title_expanded">Playlisten durchsuchen</string>
|
||||
<string name="playlist_chooser_dialog_empty">Keine Playlisten erstellt</string>
|
||||
<string name="playlist_chooser_dialog_negative_button">Abbrechen</string>
|
||||
<string name="playlist_chooser_dialog_neutral_button">Erstellen</string>
|
||||
<string name="playlist_chooser_dialog_cancel_button">Abbrechen</string>
|
||||
<string name="playlist_chooser_dialog_create_button">Erstellen</string>
|
||||
<string name="playlist_chooser_dialog_title">Zu einer Playliste hinzufügen</string>
|
||||
<string name="playlist_chooser_dialog_toast_add_success">Lied zu Playlist hinzugefügt</string>
|
||||
<string name="playlist_chooser_dialog_toast_add_failure">Titel kann nicht zur Playlist hinzugefügt werden</string>
|
||||
|
||||
@@ -212,6 +212,7 @@
|
||||
<string name="menu_unpin_button">Eliminar de la pantalla de inicio</string>
|
||||
<string name="menu_sort_year">Año</string>
|
||||
<string name="player_playback_speed">%1$.2fx</string>
|
||||
<string name="playback_speed_dialog_negative_button">Cancelar</string>
|
||||
<string name="player_queue_clean_all_button">Limpiar la cola de reproducción</string>
|
||||
<string name="player_queue_save_queue_success">Cola de reproducción guardada</string>
|
||||
<string name="player_lyrics_download_failure">La letra no se puede descargar</string>
|
||||
@@ -222,8 +223,8 @@
|
||||
<string name="playlist_catalogue_title">Catálogo de listas de reproducción</string>
|
||||
<string name="playlist_catalogue_title_expanded">Explorar listas de reproducción</string>
|
||||
<string name="playlist_chooser_dialog_empty">No hay listas de reproducción</string>
|
||||
<string name="playlist_chooser_dialog_negative_button">Cancelar</string>
|
||||
<string name="playlist_chooser_dialog_neutral_button">Crear</string>
|
||||
<string name="playlist_chooser_dialog_cancel_button">Cancelar</string>
|
||||
<string name="playlist_chooser_dialog_create_button">Crear</string>
|
||||
<string name="playlist_chooser_dialog_title">Añadir a una lista de reproducción</string>
|
||||
<string name="playlist_chooser_dialog_toast_add_failure">Error al añadir a la lista</string>
|
||||
<string name="playlist_chooser_dialog_toast_all_skipped">Todas las pistas se han descartado porque están repetidas</string>
|
||||
@@ -327,6 +328,8 @@
|
||||
<string name="settings_delete_download_storage_summary">Al continuar se eliminarán de forma irreversible todos los elementos guardados.</string>
|
||||
<string name="settings_delete_download_storage_title">Eliminar elementos guardados</string>
|
||||
<string name="settings_download_storage_title">Almacenamiento de descargas</string>
|
||||
<string name="settings_ping_timeout_summary">Establece el tiempo de espera de la URL local. Por defecto son 2 segundos (el servidor remoto usará este valor x3 hasta un máximo de 10 segundos).</string>
|
||||
<string name="settings_ping_timeout_dialog">Establece el tiempo de espera base en segundos</string>
|
||||
<string name="settings_max_bitrate_download">Tasa de bits para las descargas</string>
|
||||
<string name="settings_max_bitrate_mobile">Tasa de bits en datos móviles</string>
|
||||
<string name="settings_max_bitrate_wifi">Tasa de bits en Wi-Fi</string>
|
||||
@@ -406,6 +409,7 @@
|
||||
<string name="settings_title_transcoding">Transcodificación</string>
|
||||
<string name="settings_title_transcoding_download">Transcodificación en descargas</string>
|
||||
<string name="settings_title_ui">Interfaz de usuario</string>
|
||||
<string name="settings_title_ui_landscape_items_per_row_dialog">Número de elementos por fila</string>
|
||||
<string name="settings_transcoded_download">Descargas transcodificadas</string>
|
||||
<string name="settings_version_title">Versión</string>
|
||||
<string name="settings_wifi_only_title">Aviso de streaming solo por Wi-Fi</string>
|
||||
@@ -497,6 +501,7 @@
|
||||
<string name="settings_show_mini_shuffle_button">Mostrar el botón «Aleatorio»</string>
|
||||
<string name="settings_auto_download_lyrics">Descargar automáticamente las letras</string>
|
||||
<string name="starred_artist_sync_dialog_summary">Descargar los artistas destacados podría consumir una gran cantidad de datos.</string>
|
||||
<string name="settings_summary_landscape_items_per_row">Aplica a todos los listados de álbumes y artistas. Por defecto es 4</string>
|
||||
<string name="settings_sync_starred_artists_for_offline_use_summary">Si se habilita, los artistas destacados se descargarán para uso sin conexión.</string>
|
||||
<string name="widget_time_elapsed_placeholder">0:00</string>
|
||||
<string name="exo_controls_heart_off_description">Eliminar de favoritos</string>
|
||||
@@ -528,4 +533,7 @@
|
||||
<string name="folder_play_no_songs">No se encontraron pistas en la carpeta</string>
|
||||
<string name="search_sort_title">Ordenar las búsquedas recientes cronológicamente</string>
|
||||
<string name="search_sort_summary">Si se habilita, se ordenan las búsquedas en orden cronológico. En caso contrario, se ordenan por nombre.</string>
|
||||
<string name="settings_ping_timeout_title">Tiempo de espera de ping al servidor</string>
|
||||
<string name="playback_speed_dialog_title">Velocidad de reproducción</string>
|
||||
<string name="settings_title_ui_landscape_items_per_row">Elementos por fila en modo horizontal</string>
|
||||
</resources>
|
||||
@@ -230,8 +230,8 @@
|
||||
<string name="playlist_catalogue_title">Catalogue des Playlists</string>
|
||||
<string name="playlist_catalogue_title_expanded">Parcourir les playlists</string>
|
||||
<string name="playlist_chooser_dialog_empty">Pas de playlist</string>
|
||||
<string name="playlist_chooser_dialog_negative_button">Annuler</string>
|
||||
<string name="playlist_chooser_dialog_neutral_button">Créer</string>
|
||||
<string name="playlist_chooser_dialog_cancel_button">Annuler</string>
|
||||
<string name="playlist_chooser_dialog_create_button">Créer</string>
|
||||
<string name="playlist_chooser_dialog_title">Ajouter à une playlist</string>
|
||||
<string name="playlist_chooser_dialog_toast_add_success">Titre ajouté à la playlist</string>
|
||||
<string name="playlist_chooser_dialog_toast_add_failure">Échec d\'ajout du titre à la playlist</string>
|
||||
|
||||
@@ -223,8 +223,8 @@
|
||||
<string name="playlist_catalogue_title">Catalogo playlist</string>
|
||||
<string name="playlist_catalogue_title_expanded">Sfoglia le playlist</string>
|
||||
<string name="playlist_chooser_dialog_empty">Nessuna playlist creata</string>
|
||||
<string name="playlist_chooser_dialog_negative_button">Annulla</string>
|
||||
<string name="playlist_chooser_dialog_neutral_button">Crea</string>
|
||||
<string name="playlist_chooser_dialog_cancel_button">Annulla</string>
|
||||
<string name="playlist_chooser_dialog_create_button">Crea</string>
|
||||
<string name="playlist_chooser_dialog_title">Aggiungi a una playlist</string>
|
||||
<string name="playlist_chooser_dialog_toast_add_success">Aggiunta di un brano alla playlist</string>
|
||||
<string name="playlist_chooser_dialog_toast_add_failure">Impossibile aggiungere un brano alla playlist</string>
|
||||
|
||||
@@ -172,8 +172,8 @@
|
||||
<string name="playlist_catalogue_title">플레이리스트 카탈로그</string>
|
||||
<string name="playlist_catalogue_title_expanded">플레이리스트 찾아보기</string>
|
||||
<string name="playlist_chooser_dialog_empty">플레이리스트가 없습니다.</string>
|
||||
<string name="playlist_chooser_dialog_negative_button">취소</string>
|
||||
<string name="playlist_chooser_dialog_neutral_button">생성</string>
|
||||
<string name="playlist_chooser_dialog_cancel_button">취소</string>
|
||||
<string name="playlist_chooser_dialog_create_button">생성</string>
|
||||
<string name="playlist_chooser_dialog_title">플레이리스트 추가</string>
|
||||
<string name="playlist_chooser_dialog_toast_add_success">재생 목록에 음악 추가</string>
|
||||
<string name="playlist_chooser_dialog_toast_add_failure">재생 목록에 음악을 추가하지 못했습니다.</string>
|
||||
|
||||
@@ -222,8 +222,8 @@
|
||||
<string name="playlist_catalogue_title">Katalog Playlist</string>
|
||||
<string name="playlist_catalogue_title_expanded">Przeglądaj Playlisty</string>
|
||||
<string name="playlist_chooser_dialog_empty">Nie utworzono playlist</string>
|
||||
<string name="playlist_chooser_dialog_negative_button">Anuluj</string>
|
||||
<string name="playlist_chooser_dialog_neutral_button">Utwórz</string>
|
||||
<string name="playlist_chooser_dialog_cancel_button">Anuluj</string>
|
||||
<string name="playlist_chooser_dialog_create_button">Utwórz</string>
|
||||
<string name="playlist_chooser_dialog_title">Dodaj do playlisty</string>
|
||||
<string name="playlist_chooser_dialog_toast_add_success">Dodano piosenki do playlisty</string>
|
||||
<string name="playlist_chooser_dialog_toast_add_failure">Nie udało się dodać piosenek do playlisty</string>
|
||||
|
||||
@@ -159,8 +159,8 @@
|
||||
<string name="playlist_catalogue_title">Catálogo de Playlists</string>
|
||||
<string name="playlist_catalogue_title_expanded">Navegar pelas Playlists</string>
|
||||
<string name="playlist_chooser_dialog_empty">Nenhuma playlist criada</string>
|
||||
<string name="playlist_chooser_dialog_negative_button">Cancelar</string>
|
||||
<string name="playlist_chooser_dialog_neutral_button">Criar</string>
|
||||
<string name="playlist_chooser_dialog_cancel_button">Cancelar</string>
|
||||
<string name="playlist_chooser_dialog_create_button">Criar</string>
|
||||
<string name="playlist_chooser_dialog_title">Adicionar a uma playlist</string>
|
||||
<string name="playlist_chooser_dialog_toast_add_success">Adicionada playlist de reprodução</string>
|
||||
<string name="playlist_chooser_dialog_toast_add_failure">Falha ao adicionar uma playlist de reprodução</string>
|
||||
|
||||
@@ -233,8 +233,8 @@
|
||||
<string name="playlist_catalogue_title">Catalogul Playlisturi</string>
|
||||
<string name="playlist_catalogue_title_expanded">Răsfoiți Playlisturi</string>
|
||||
<string name="playlist_chooser_dialog_empty">Niciun playlist creat</string>
|
||||
<string name="playlist_chooser_dialog_negative_button">Anulati</string>
|
||||
<string name="playlist_chooser_dialog_neutral_button">Creaţi</string>
|
||||
<string name="playlist_chooser_dialog_cancel_button">Anulati</string>
|
||||
<string name="playlist_chooser_dialog_create_button">Creaţi</string>
|
||||
<string name="playlist_chooser_dialog_title">Adăugați la un playlist</string>
|
||||
<string name="playlist_chooser_dialog_toast_add_success">Piesa(e) adăugată(e) la playlist</string>
|
||||
<string name="playlist_chooser_dialog_toast_add_failure">Eșec la adăugarea piese(lor) la playlist</string>
|
||||
|
||||
@@ -200,8 +200,8 @@
|
||||
<string name="playlist_catalogue_title">Каталог плейлистов</string>
|
||||
<string name="playlist_catalogue_title_expanded">Просмотр плейлистов</string>
|
||||
<string name="playlist_chooser_dialog_empty">Плейлисты не созданы</string>
|
||||
<string name="playlist_chooser_dialog_negative_button">Отмена</string>
|
||||
<string name="playlist_chooser_dialog_neutral_button">Создать</string>
|
||||
<string name="playlist_chooser_dialog_cancel_button">Отмена</string>
|
||||
<string name="playlist_chooser_dialog_create_button">Создать</string>
|
||||
<string name="playlist_chooser_dialog_title">Добавить в плейлист</string>
|
||||
<string name="playlist_chooser_dialog_toast_add_success">Добавьте песню в плейлист</string>
|
||||
<string name="playlist_chooser_dialog_toast_add_failure">Не удалось добавить песню в список воспроизведения</string>
|
||||
|
||||
@@ -203,8 +203,8 @@
|
||||
<string name="playlist_catalogue_title">Çalma Listesi Kataloğu</string>
|
||||
<string name="playlist_catalogue_title_expanded">Çalma listelerine göz at</string>
|
||||
<string name="playlist_chooser_dialog_empty">Henüz çalma listesi oluşturulmadı</string>
|
||||
<string name="playlist_chooser_dialog_negative_button">İptal</string>
|
||||
<string name="playlist_chooser_dialog_neutral_button">Oluştur</string>
|
||||
<string name="playlist_chooser_dialog_cancel_button">İptal</string>
|
||||
<string name="playlist_chooser_dialog_create_button">Oluştur</string>
|
||||
<string name="playlist_chooser_dialog_title">Çalma listesine ekle</string>
|
||||
<string name="playlist_chooser_dialog_toast_add_success">Şarkı çalma listesine eklendi</string>
|
||||
<string name="playlist_chooser_dialog_toast_add_failure">Şarkı çalma listesine eklenemedi</string>
|
||||
|
||||
@@ -260,8 +260,8 @@
|
||||
<string name="playlist_catalogue_title">播放列表目录</string>
|
||||
<string name="playlist_catalogue_title_expanded">浏览播放列表</string>
|
||||
<string name="playlist_chooser_dialog_empty">尚未创建播放列表</string>
|
||||
<string name="playlist_chooser_dialog_negative_button">取消</string>
|
||||
<string name="playlist_chooser_dialog_neutral_button">新建</string>
|
||||
<string name="playlist_chooser_dialog_cancel_button">取消</string>
|
||||
<string name="playlist_chooser_dialog_create_button">新建</string>
|
||||
<string name="playlist_chooser_dialog_title">添加到播放列表</string>
|
||||
<string name="playlist_chooser_dialog_toast_add_failure">未能将歌曲添加到播放列表</string>
|
||||
<string name="playlist_chooser_dialog_toast_add_success">将歌曲添加到播放列表</string>
|
||||
|
||||
@@ -234,14 +234,16 @@
|
||||
<string name="playlist_catalogue_title">Playlist Catalogue</string>
|
||||
<string name="playlist_catalogue_title_expanded">Browse Playlists</string>
|
||||
<string name="playlist_chooser_dialog_empty">No playlists created</string>
|
||||
<string name="playlist_chooser_dialog_negative_button">Cancel</string>
|
||||
<string name="playlist_chooser_dialog_neutral_button">Create</string>
|
||||
<string name="playlist_chooser_dialog_cancel_button">Cancel</string>
|
||||
<string name="playlist_chooser_dialog_create_button">Create</string>
|
||||
<string name="playlist_chooser_dialog_title">Add to a playlist</string>
|
||||
<string name="playlist_chooser_dialog_toast_add_success">Added song(s) to playlist</string>
|
||||
<string name="playlist_chooser_dialog_toast_add_failure">Failed to add song(s) to playlist</string>
|
||||
<string name="playlist_chooser_dialog_toast_all_skipped">All songs were skipped as duplicates</string>
|
||||
<string name="playlist_chooser_dialog_visibility_public">Public</string>
|
||||
<string name="playlist_chooser_dialog_visibility_private">Private</string>
|
||||
<string name="playlist_chooser_dialog_visibility_switch_label">Mark the playlist as public</string>
|
||||
<string name="playlist_chooser_dialog_visibility_summary">The server updates the visibility on each request. By default it is set to private.</string>
|
||||
<string name="playlist_counted_tracks">%1$d tracks • %2$s</string>
|
||||
<string name="playlist_duration">Duration • %1$s</string>
|
||||
<string name="playlist_editor_dialog_action_delete_toast">Long press to delete</string>
|
||||
@@ -517,6 +519,7 @@
|
||||
<string name="track_info_summary_transcoding_codec">The application will request the server to transcode the file. The requested codec by the user is %1$s, while the bitrate will be the same as the source file. The potential transcoding of the file into the chosen format is dependent on the server, as it may or may not support the operation.</string>
|
||||
<string name="track_info_title">Title</string>
|
||||
<string name="track_info_track_number">Track number</string>
|
||||
<string name="track_info_station">Station</string>
|
||||
<string name="track_info_transcoded_content_type">Transcoded content type</string>
|
||||
<string name="track_info_transcoded_suffix">Transcoded suffix</string>
|
||||
<string name="track_info_year">Year</string>
|
||||
|
||||
@@ -32,6 +32,7 @@ import com.cappielloantonio.tempo.util.Constants.CUSTOM_COMMAND_TOGGLE_REPEAT_MO
|
||||
import com.cappielloantonio.tempo.util.Constants.CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF
|
||||
import com.cappielloantonio.tempo.util.Constants.CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON
|
||||
import com.google.common.collect.ImmutableList
|
||||
import com.cappielloantonio.tempo.util.Constants
|
||||
import com.cappielloantonio.tempo.util.Preferences
|
||||
import com.google.common.util.concurrent.Futures
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
@@ -40,8 +41,8 @@ import retrofit2.Callback
|
||||
import retrofit2.Response
|
||||
|
||||
open class MediaLibrarySessionCallback(
|
||||
context: Context,
|
||||
automotiveRepository: AutomotiveRepository
|
||||
private val context: Context,
|
||||
private val automotiveRepository: AutomotiveRepository
|
||||
) :
|
||||
MediaLibraryService.MediaLibrarySession.Callback {
|
||||
|
||||
@@ -366,11 +367,31 @@ open class MediaLibrarySessionCallback(
|
||||
controller: MediaSession.ControllerInfo,
|
||||
mediaItems: List<MediaItem>
|
||||
): ListenableFuture<List<MediaItem>> {
|
||||
return super.onAddMediaItems(
|
||||
mediaSession,
|
||||
controller,
|
||||
MediaBrowserTree.getItems(mediaItems)
|
||||
)
|
||||
val firstItem = mediaItems.firstOrNull()
|
||||
val isRadio = firstItem?.mediaId?.startsWith("ir-") == true
|
||||
|
||||
if (isRadio) {
|
||||
return Futures.transformAsync(
|
||||
automotiveRepository.internetRadioStations,
|
||||
{ result ->
|
||||
val stations = result?.value
|
||||
val selected = stations?.find { it.mediaId == firstItem?.mediaId }
|
||||
if (selected != null) {
|
||||
val updatedSelected = selected.buildUpon()
|
||||
.setMimeType(selected.localConfiguration?.mimeType)
|
||||
.build()
|
||||
|
||||
Futures.immediateFuture(listOf(updatedSelected))
|
||||
} else {
|
||||
Futures.immediateFuture(emptyList())
|
||||
}
|
||||
},
|
||||
androidx.core.content.ContextCompat.getMainExecutor(context)
|
||||
)
|
||||
}
|
||||
|
||||
val resolvedItems = MediaBrowserTree.getItems(mediaItems)
|
||||
return super.onAddMediaItems(mediaSession, controller, resolvedItems)
|
||||
}
|
||||
|
||||
override fun onSearch(
|
||||
|
||||
4
fastlane/metadata/android/en-US/changelogs/19.txt
Normal file
4
fastlane/metadata/android/en-US/changelogs/19.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
fix: Addressing some UI/UX quirks
|
||||
fix: keep observer until data is received on continuousPlay bug
|
||||
fix: album art now displays on android auto
|
||||
feat: improve landscape view and increase items per row on landscape view
|
||||
10
fastlane/metadata/android/en-US/changelogs/20.txt
Normal file
10
fastlane/metadata/android/en-US/changelogs/20.txt
Normal file
@@ -0,0 +1,10 @@
|
||||
fix: added dynamic application id from gradle variant
|
||||
fix: Use Bluetooth tethering connection
|
||||
chore(i18n): Update Spanish translation
|
||||
fix: visual glitches on landscape navbar
|
||||
fix: radio playback "source error" on android auto
|
||||
fix: speed button overlaps with shuffle on landscape
|
||||
fix: local url used in share link instead of server url
|
||||
Feat: prefer downloaded files
|
||||
fix: radio metadata displayed
|
||||
feat: improve playlist chooser dialog UI
|
||||
Reference in New Issue
Block a user