mirror of
https://github.com/antebudimir/tempus.git
synced 2025-12-31 09:33:33 +00:00
Merge branch 'development' into library-play
This commit is contained in:
commit
3496918ce6
14 changed files with 1237 additions and 1449 deletions
|
|
@ -10,8 +10,8 @@ android {
|
|||
minSdkVersion 24
|
||||
targetSdk 35
|
||||
|
||||
versionCode 6
|
||||
versionName '4.2.4'
|
||||
versionCode 7
|
||||
versionName '4.2.6'
|
||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||
|
||||
javaCompileOptions {
|
||||
|
|
|
|||
|
|
@ -1,561 +1,6 @@
|
|||
package com.cappielloantonio.tempo.service
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.PendingIntent.FLAG_IMMUTABLE
|
||||
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
|
||||
import android.app.TaskStackBuilder
|
||||
import android.content.Intent
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.Network
|
||||
import android.net.NetworkCapabilities
|
||||
import android.os.Binder
|
||||
import android.os.Bundle
|
||||
import android.os.IBinder
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import androidx.media3.common.*
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.exoplayer.DefaultLoadControl
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import androidx.media3.exoplayer.source.MediaSource
|
||||
import androidx.media3.session.*
|
||||
import androidx.media3.session.MediaSession.ControllerInfo
|
||||
import com.cappielloantonio.tempo.R
|
||||
import com.cappielloantonio.tempo.repository.QueueRepository
|
||||
import com.cappielloantonio.tempo.ui.activity.MainActivity
|
||||
import com.cappielloantonio.tempo.util.AssetLinkUtil
|
||||
import com.cappielloantonio.tempo.util.Constants
|
||||
import com.cappielloantonio.tempo.util.DownloadUtil
|
||||
import com.cappielloantonio.tempo.util.DynamicMediaSourceFactory
|
||||
import com.cappielloantonio.tempo.util.MappingUtil
|
||||
import com.cappielloantonio.tempo.util.Preferences
|
||||
import com.cappielloantonio.tempo.util.ReplayGainUtil
|
||||
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
|
||||
|
||||
|
||||
@UnstableApi
|
||||
class MediaService : MediaLibraryService() {
|
||||
private val librarySessionCallback = CustomMediaLibrarySessionCallback()
|
||||
|
||||
private lateinit var player: ExoPlayer
|
||||
private lateinit var mediaLibrarySession: MediaLibrarySession
|
||||
private lateinit var shuffleCommands: List<CommandButton>
|
||||
private lateinit var repeatCommands: List<CommandButton>
|
||||
private lateinit var networkCallback: CustomNetworkCallback
|
||||
lateinit var equalizerManager: EqualizerManager
|
||||
|
||||
private var customLayout = ImmutableList.of<CommandButton>()
|
||||
private val widgetUpdateHandler = Handler(Looper.getMainLooper())
|
||||
private var widgetUpdateScheduled = false
|
||||
private val widgetUpdateRunnable = object : Runnable {
|
||||
override fun run() {
|
||||
if (!player.isPlaying) {
|
||||
widgetUpdateScheduled = false
|
||||
return
|
||||
}
|
||||
updateWidget()
|
||||
widgetUpdateHandler.postDelayed(this, WIDGET_UPDATE_INTERVAL_MS)
|
||||
}
|
||||
}
|
||||
|
||||
inner class LocalBinder : Binder() {
|
||||
fun getEqualizerManager(): EqualizerManager {
|
||||
return this@MediaService.equalizerManager
|
||||
}
|
||||
}
|
||||
|
||||
private val binder = LocalBinder()
|
||||
|
||||
companion object {
|
||||
private const val CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON =
|
||||
"android.media3.session.demo.SHUFFLE_ON"
|
||||
private const val CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF =
|
||||
"android.media3.session.demo.SHUFFLE_OFF"
|
||||
private const val CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_OFF =
|
||||
"android.media3.session.demo.REPEAT_OFF"
|
||||
private const val CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ONE =
|
||||
"android.media3.session.demo.REPEAT_ONE"
|
||||
private const val CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ALL =
|
||||
"android.media3.session.demo.REPEAT_ALL"
|
||||
const val ACTION_BIND_EQUALIZER = "com.cappielloantonio.tempo.service.BIND_EQUALIZER"
|
||||
const val ACTION_EQUALIZER_UPDATED = "com.cappielloantonio.tempo.service.EQUALIZER_UPDATED"
|
||||
}
|
||||
|
||||
fun updateMediaItems() {
|
||||
Log.d("MediaService", "update items");
|
||||
val n = player.mediaItemCount
|
||||
val k = player.currentMediaItemIndex
|
||||
val current = player.currentPosition
|
||||
val items = (0 .. n-1).map{i -> MappingUtil.mapMediaItem(player.getMediaItemAt(i))}
|
||||
player.clearMediaItems()
|
||||
player.setMediaItems(items, k, current)
|
||||
}
|
||||
|
||||
inner class CustomNetworkCallback : ConnectivityManager.NetworkCallback() {
|
||||
var wasWifi = false
|
||||
|
||||
init {
|
||||
val manager = getSystemService(ConnectivityManager::class.java)
|
||||
val network = manager.activeNetwork
|
||||
val capabilities = manager.getNetworkCapabilities(network)
|
||||
if (capabilities != null)
|
||||
wasWifi = capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
|
||||
}
|
||||
|
||||
override fun onCapabilitiesChanged(network : Network, networkCapabilities : NetworkCapabilities) {
|
||||
val isWifi = networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
|
||||
if (isWifi != wasWifi) {
|
||||
wasWifi = isWifi
|
||||
widgetUpdateHandler.post(Runnable {
|
||||
updateMediaItems()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
initializeCustomCommands()
|
||||
initializePlayer()
|
||||
initializeMediaLibrarySession()
|
||||
restorePlayerFromQueue()
|
||||
initializePlayerListener()
|
||||
initializeEqualizerManager()
|
||||
initializeNetworkListener()
|
||||
|
||||
setPlayer(player)
|
||||
}
|
||||
|
||||
override fun onGetSession(controllerInfo: ControllerInfo): MediaLibrarySession {
|
||||
return mediaLibrarySession
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
releaseNetworkCallback()
|
||||
equalizerManager.release()
|
||||
stopWidgetUpdates()
|
||||
releasePlayer()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? {
|
||||
// Check if the intent is for our custom equalizer binder
|
||||
if (intent?.action == ACTION_BIND_EQUALIZER) {
|
||||
return binder
|
||||
}
|
||||
// Otherwise, handle it as a normal MediaLibraryService connection
|
||||
return super.onBind(intent)
|
||||
}
|
||||
|
||||
private inner class CustomMediaLibrarySessionCallback : MediaLibrarySession.Callback {
|
||||
|
||||
override fun onConnect(
|
||||
session: MediaSession,
|
||||
controller: ControllerInfo
|
||||
): MediaSession.ConnectionResult {
|
||||
val connectionResult = super.onConnect(session, controller)
|
||||
val availableSessionCommands = connectionResult.availableSessionCommands.buildUpon()
|
||||
|
||||
(shuffleCommands + repeatCommands).forEach { commandButton ->
|
||||
commandButton.sessionCommand?.let { availableSessionCommands.add(it) }
|
||||
}
|
||||
|
||||
customLayout = buildCustomLayout(session.player)
|
||||
|
||||
return MediaSession.ConnectionResult.AcceptedResultBuilder(session)
|
||||
.setAvailableSessionCommands(availableSessionCommands.build())
|
||||
.setAvailablePlayerCommands(connectionResult.availablePlayerCommands)
|
||||
.setCustomLayout(customLayout)
|
||||
.build()
|
||||
}
|
||||
|
||||
override fun onPostConnect(session: MediaSession, controller: ControllerInfo) {
|
||||
if (!customLayout.isEmpty() && controller.controllerVersion != 0) {
|
||||
ignoreFuture(mediaLibrarySession.setCustomLayout(controller, customLayout))
|
||||
}
|
||||
}
|
||||
|
||||
fun buildCustomLayout(player: Player): ImmutableList<CommandButton> {
|
||||
val shuffle = shuffleCommands[if (player.shuffleModeEnabled) 1 else 0]
|
||||
val repeat = when (player.repeatMode) {
|
||||
Player.REPEAT_MODE_ONE -> repeatCommands[1]
|
||||
Player.REPEAT_MODE_ALL -> repeatCommands[2]
|
||||
else -> repeatCommands[0]
|
||||
}
|
||||
return ImmutableList.of(shuffle, repeat)
|
||||
}
|
||||
|
||||
override fun onCustomCommand(
|
||||
session: MediaSession,
|
||||
controller: ControllerInfo,
|
||||
customCommand: SessionCommand,
|
||||
args: Bundle
|
||||
): ListenableFuture<SessionResult> {
|
||||
when (customCommand.customAction) {
|
||||
CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON -> player.shuffleModeEnabled = true
|
||||
CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF -> player.shuffleModeEnabled = false
|
||||
CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_OFF,
|
||||
CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ALL,
|
||||
CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ONE -> {
|
||||
val nextMode = when (player.repeatMode) {
|
||||
Player.REPEAT_MODE_ONE -> Player.REPEAT_MODE_ALL
|
||||
Player.REPEAT_MODE_OFF -> Player.REPEAT_MODE_ONE
|
||||
else -> Player.REPEAT_MODE_OFF
|
||||
}
|
||||
player.repeatMode = nextMode
|
||||
}
|
||||
}
|
||||
|
||||
customLayout = librarySessionCallback.buildCustomLayout(player)
|
||||
session.setCustomLayout(customLayout)
|
||||
|
||||
return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
|
||||
}
|
||||
|
||||
override fun onAddMediaItems(
|
||||
mediaSession: MediaSession,
|
||||
controller: ControllerInfo,
|
||||
mediaItems: List<MediaItem>
|
||||
): ListenableFuture<List<MediaItem>> {
|
||||
val updatedMediaItems = mediaItems.map { mediaItem ->
|
||||
val mediaMetadata = mediaItem.mediaMetadata
|
||||
|
||||
val newMetadata = mediaMetadata.buildUpon()
|
||||
.setArtist(
|
||||
if (mediaMetadata.artist != null) mediaMetadata.artist
|
||||
else mediaMetadata.extras?.getString("uri") ?: ""
|
||||
)
|
||||
.build()
|
||||
|
||||
mediaItem.buildUpon()
|
||||
.setUri(mediaItem.requestMetadata.mediaUri)
|
||||
.setMediaMetadata(newMetadata)
|
||||
.setMimeType(MimeTypes.BASE_TYPE_AUDIO)
|
||||
.build()
|
||||
}
|
||||
return Futures.immediateFuture(updatedMediaItems)
|
||||
}
|
||||
}
|
||||
|
||||
private fun initializeCustomCommands() {
|
||||
shuffleCommands = listOf(
|
||||
getShuffleCommandButton(
|
||||
SessionCommand(CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON, Bundle.EMPTY)
|
||||
),
|
||||
getShuffleCommandButton(
|
||||
SessionCommand(CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF, Bundle.EMPTY)
|
||||
)
|
||||
)
|
||||
|
||||
repeatCommands = listOf(
|
||||
getRepeatCommandButton(
|
||||
SessionCommand(CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_OFF, Bundle.EMPTY)
|
||||
),
|
||||
getRepeatCommandButton(
|
||||
SessionCommand(CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ONE, Bundle.EMPTY)
|
||||
),
|
||||
getRepeatCommandButton(
|
||||
SessionCommand(CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ALL, Bundle.EMPTY)
|
||||
)
|
||||
)
|
||||
|
||||
customLayout = ImmutableList.of(shuffleCommands[0], repeatCommands[0])
|
||||
}
|
||||
|
||||
private fun initializePlayer() {
|
||||
player = ExoPlayer.Builder(this)
|
||||
.setRenderersFactory(getRenderersFactory())
|
||||
.setMediaSourceFactory(getMediaSourceFactory())
|
||||
.setAudioAttributes(AudioAttributes.DEFAULT, true)
|
||||
.setHandleAudioBecomingNoisy(true)
|
||||
.setWakeMode(C.WAKE_MODE_NETWORK)
|
||||
.setLoadControl(initializeLoadControl())
|
||||
.build()
|
||||
|
||||
player.shuffleModeEnabled = Preferences.isShuffleModeEnabled()
|
||||
player.repeatMode = Preferences.getRepeatMode()
|
||||
}
|
||||
|
||||
private fun initializeEqualizerManager() {
|
||||
equalizerManager = EqualizerManager()
|
||||
val audioSessionId = player.audioSessionId
|
||||
attachEqualizerIfPossible(audioSessionId)
|
||||
}
|
||||
|
||||
private fun initializeMediaLibrarySession() {
|
||||
val sessionActivityPendingIntent =
|
||||
TaskStackBuilder.create(this).run {
|
||||
addNextIntent(Intent(this@MediaService, MainActivity::class.java))
|
||||
getPendingIntent(0, FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT)
|
||||
}
|
||||
|
||||
mediaLibrarySession =
|
||||
MediaLibrarySession.Builder(this, player, librarySessionCallback)
|
||||
.setSessionActivity(sessionActivityPendingIntent)
|
||||
.build()
|
||||
|
||||
if (!customLayout.isEmpty()) {
|
||||
mediaLibrarySession.setCustomLayout(customLayout)
|
||||
}
|
||||
}
|
||||
|
||||
private fun initializeNetworkListener() {
|
||||
networkCallback = CustomNetworkCallback()
|
||||
getSystemService(ConnectivityManager::class.java).registerDefaultNetworkCallback(networkCallback)
|
||||
updateMediaItems()
|
||||
}
|
||||
|
||||
private fun restorePlayerFromQueue() {
|
||||
if (player.mediaItemCount > 0) return
|
||||
|
||||
val queueRepository = QueueRepository()
|
||||
val storedQueue = queueRepository.media
|
||||
if (storedQueue.isNullOrEmpty()) return
|
||||
|
||||
val mediaItems = MappingUtil.mapMediaItems(storedQueue)
|
||||
if (mediaItems.isEmpty()) return
|
||||
|
||||
val lastIndex = try {
|
||||
queueRepository.lastPlayedMediaIndex
|
||||
} catch (_: Exception) {
|
||||
0
|
||||
}.coerceIn(0, mediaItems.size - 1)
|
||||
|
||||
val lastPosition = try {
|
||||
queueRepository.lastPlayedMediaTimestamp
|
||||
} catch (_: Exception) {
|
||||
0L
|
||||
}.let { if (it < 0L) 0L else it }
|
||||
|
||||
player.setMediaItems(mediaItems, lastIndex, lastPosition)
|
||||
player.prepare()
|
||||
updateWidget()
|
||||
}
|
||||
|
||||
private fun initializePlayerListener() {
|
||||
player.addListener(object : Player.Listener {
|
||||
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
|
||||
if (mediaItem == null) return
|
||||
|
||||
if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_SEEK || reason == Player.MEDIA_ITEM_TRANSITION_REASON_AUTO) {
|
||||
MediaManager.setLastPlayedTimestamp(mediaItem)
|
||||
}
|
||||
updateWidget()
|
||||
}
|
||||
|
||||
override fun onTracksChanged(tracks: Tracks) {
|
||||
ReplayGainUtil.setReplayGain(player, tracks)
|
||||
val currentMediaItem = player.currentMediaItem
|
||||
if (currentMediaItem != null && currentMediaItem.mediaMetadata.extras != null) {
|
||||
MediaManager.scrobble(currentMediaItem, false)
|
||||
}
|
||||
|
||||
if (player.currentMediaItemIndex + 1 == player.mediaItemCount)
|
||||
MediaManager.continuousPlay(player.currentMediaItem)
|
||||
}
|
||||
|
||||
override fun onIsPlayingChanged(isPlaying: Boolean) {
|
||||
if (!isPlaying) {
|
||||
MediaManager.setPlayingPausedTimestamp(
|
||||
player.currentMediaItem,
|
||||
player.currentPosition
|
||||
)
|
||||
} else {
|
||||
MediaManager.scrobble(player.currentMediaItem, false)
|
||||
}
|
||||
if (isPlaying) {
|
||||
scheduleWidgetUpdates()
|
||||
} else {
|
||||
stopWidgetUpdates()
|
||||
}
|
||||
updateWidget()
|
||||
}
|
||||
|
||||
override fun onPlaybackStateChanged(playbackState: Int) {
|
||||
super.onPlaybackStateChanged(playbackState)
|
||||
if (!player.hasNextMediaItem() &&
|
||||
playbackState == Player.STATE_ENDED &&
|
||||
player.mediaMetadata.extras?.getString("type") == Constants.MEDIA_TYPE_MUSIC
|
||||
) {
|
||||
MediaManager.scrobble(player.currentMediaItem, true)
|
||||
MediaManager.saveChronology(player.currentMediaItem)
|
||||
}
|
||||
updateWidget()
|
||||
}
|
||||
|
||||
override fun onPositionDiscontinuity(
|
||||
oldPosition: Player.PositionInfo,
|
||||
newPosition: Player.PositionInfo,
|
||||
reason: Int
|
||||
) {
|
||||
super.onPositionDiscontinuity(oldPosition, newPosition, reason)
|
||||
|
||||
if (reason == Player.DISCONTINUITY_REASON_AUTO_TRANSITION) {
|
||||
if (oldPosition.mediaItem?.mediaMetadata?.extras?.getString("type") == Constants.MEDIA_TYPE_MUSIC) {
|
||||
MediaManager.scrobble(oldPosition.mediaItem, true)
|
||||
MediaManager.saveChronology(oldPosition.mediaItem)
|
||||
}
|
||||
|
||||
if (newPosition.mediaItem?.mediaMetadata?.extras?.getString("type") == Constants.MEDIA_TYPE_MUSIC) {
|
||||
MediaManager.setLastPlayedTimestamp(newPosition.mediaItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onShuffleModeEnabledChanged(shuffleModeEnabled: Boolean) {
|
||||
Preferences.setShuffleModeEnabled(shuffleModeEnabled)
|
||||
customLayout = librarySessionCallback.buildCustomLayout(player)
|
||||
mediaLibrarySession.setCustomLayout(customLayout)
|
||||
}
|
||||
|
||||
override fun onRepeatModeChanged(repeatMode: Int) {
|
||||
Preferences.setRepeatMode(repeatMode)
|
||||
customLayout = librarySessionCallback.buildCustomLayout(player)
|
||||
mediaLibrarySession.setCustomLayout(customLayout)
|
||||
}
|
||||
|
||||
override fun onAudioSessionIdChanged(audioSessionId: Int) {
|
||||
attachEqualizerIfPossible(audioSessionId)
|
||||
}
|
||||
})
|
||||
if (player.isPlaying) {
|
||||
scheduleWidgetUpdates()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setPlayer(player: Player) {
|
||||
mediaLibrarySession.player = player
|
||||
}
|
||||
|
||||
private fun releasePlayer() {
|
||||
player.release()
|
||||
mediaLibrarySession.release()
|
||||
}
|
||||
|
||||
private fun releaseNetworkCallback() {
|
||||
getSystemService(ConnectivityManager::class.java).unregisterNetworkCallback(networkCallback)
|
||||
}
|
||||
|
||||
@SuppressLint("PrivateResource")
|
||||
private fun getShuffleCommandButton(sessionCommand: SessionCommand): CommandButton {
|
||||
val isOn = sessionCommand.customAction == CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON
|
||||
return CommandButton.Builder()
|
||||
.setDisplayName(
|
||||
getString(
|
||||
if (isOn) R.string.exo_controls_shuffle_on_description
|
||||
else R.string.exo_controls_shuffle_off_description
|
||||
)
|
||||
)
|
||||
.setSessionCommand(sessionCommand)
|
||||
.setIconResId(if (isOn) R.drawable.exo_icon_shuffle_off else R.drawable.exo_icon_shuffle_on)
|
||||
.build()
|
||||
}
|
||||
|
||||
@SuppressLint("PrivateResource")
|
||||
private fun getRepeatCommandButton(sessionCommand: SessionCommand): CommandButton {
|
||||
val icon = when (sessionCommand.customAction) {
|
||||
CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ONE -> R.drawable.exo_icon_repeat_one
|
||||
CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ALL -> R.drawable.exo_icon_repeat_all
|
||||
else -> R.drawable.exo_icon_repeat_off
|
||||
}
|
||||
val description = when (sessionCommand.customAction) {
|
||||
CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ONE -> R.string.exo_controls_repeat_one_description
|
||||
CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ALL -> R.string.exo_controls_repeat_all_description
|
||||
else -> R.string.exo_controls_repeat_off_description
|
||||
}
|
||||
return CommandButton.Builder()
|
||||
.setDisplayName(getString(description))
|
||||
.setSessionCommand(sessionCommand)
|
||||
.setIconResId(icon)
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun ignoreFuture(@Suppress("UNUSED_PARAMETER") customLayout: ListenableFuture<SessionResult>) {
|
||||
/* Do nothing. */
|
||||
}
|
||||
|
||||
private fun initializeLoadControl(): DefaultLoadControl {
|
||||
return DefaultLoadControl.Builder()
|
||||
.setBufferDurationsMs(
|
||||
(DefaultLoadControl.DEFAULT_MIN_BUFFER_MS * Preferences.getBufferingStrategy()).toInt(),
|
||||
(DefaultLoadControl.DEFAULT_MAX_BUFFER_MS * Preferences.getBufferingStrategy()).toInt(),
|
||||
DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS,
|
||||
DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS
|
||||
)
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun updateWidget() {
|
||||
val mi = player.currentMediaItem
|
||||
val title = mi?.mediaMetadata?.title?.toString()
|
||||
?: mi?.mediaMetadata?.extras?.getString("title")
|
||||
val artist = mi?.mediaMetadata?.artist?.toString()
|
||||
?: mi?.mediaMetadata?.extras?.getString("artist")
|
||||
val album = mi?.mediaMetadata?.albumTitle?.toString()
|
||||
?: mi?.mediaMetadata?.extras?.getString("album")
|
||||
val extras = mi?.mediaMetadata?.extras
|
||||
val coverId = extras?.getString("coverArtId")
|
||||
val songLink = extras?.getString("assetLinkSong")
|
||||
?: AssetLinkUtil.buildLink(AssetLinkUtil.TYPE_SONG, extras?.getString("id"))
|
||||
val albumLink = extras?.getString("assetLinkAlbum")
|
||||
?: AssetLinkUtil.buildLink(AssetLinkUtil.TYPE_ALBUM, extras?.getString("albumId"))
|
||||
val artistLink = extras?.getString("assetLinkArtist")
|
||||
?: AssetLinkUtil.buildLink(AssetLinkUtil.TYPE_ARTIST, extras?.getString("artistId"))
|
||||
val position = player.currentPosition.takeIf { it != C.TIME_UNSET } ?: 0L
|
||||
val duration = player.duration.takeIf { it != C.TIME_UNSET } ?: 0L
|
||||
WidgetUpdateManager.updateFromState(
|
||||
this,
|
||||
title ?: "",
|
||||
artist ?: "",
|
||||
album ?: "",
|
||||
coverId,
|
||||
player.isPlaying,
|
||||
player.shuffleModeEnabled,
|
||||
player.repeatMode,
|
||||
position,
|
||||
duration,
|
||||
songLink,
|
||||
albumLink,
|
||||
artistLink
|
||||
)
|
||||
}
|
||||
|
||||
private fun scheduleWidgetUpdates() {
|
||||
if (widgetUpdateScheduled) return
|
||||
widgetUpdateHandler.postDelayed(widgetUpdateRunnable, WIDGET_UPDATE_INTERVAL_MS)
|
||||
widgetUpdateScheduled = true
|
||||
}
|
||||
|
||||
private fun stopWidgetUpdates() {
|
||||
if (!widgetUpdateScheduled) return
|
||||
widgetUpdateHandler.removeCallbacks(widgetUpdateRunnable)
|
||||
widgetUpdateScheduled = false
|
||||
}
|
||||
|
||||
private fun attachEqualizerIfPossible(audioSessionId: Int): Boolean {
|
||||
if (audioSessionId == 0 || audioSessionId == -1) return false
|
||||
val attached = equalizerManager.attachToSession(audioSessionId)
|
||||
if (attached) {
|
||||
val enabled = Preferences.isEqualizerEnabled()
|
||||
equalizerManager.setEnabled(enabled)
|
||||
val bands = equalizerManager.getNumberOfBands()
|
||||
val savedLevels = Preferences.getEqualizerBandLevels(bands)
|
||||
for (i in 0 until bands) {
|
||||
equalizerManager.setBandLevel(i.toShort(), savedLevels[i])
|
||||
}
|
||||
sendBroadcast(Intent(ACTION_EQUALIZER_UPDATED))
|
||||
}
|
||||
return attached
|
||||
}
|
||||
|
||||
private fun getRenderersFactory() = DownloadUtil.buildRenderersFactory(this, false)
|
||||
|
||||
private fun getMediaSourceFactory(): MediaSource.Factory = DynamicMediaSourceFactory(this)
|
||||
}
|
||||
|
||||
private const val WIDGET_UPDATE_INTERVAL_MS = 1000L
|
||||
class MediaService : BaseMediaService()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,590 @@
|
|||
package com.cappielloantonio.tempo.service
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.PendingIntent.FLAG_IMMUTABLE
|
||||
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
|
||||
import android.app.TaskStackBuilder
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.Network
|
||||
import android.net.NetworkCapabilities
|
||||
import android.os.Binder
|
||||
import android.os.Bundle
|
||||
import android.os.IBinder
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import androidx.media3.common.*
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.exoplayer.DefaultLoadControl
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import androidx.media3.exoplayer.source.MediaSource
|
||||
import androidx.media3.exoplayer.source.ShuffleOrder.DefaultShuffleOrder
|
||||
import androidx.media3.session.*
|
||||
import androidx.media3.session.MediaSession.ControllerInfo
|
||||
import com.cappielloantonio.tempo.R
|
||||
import com.cappielloantonio.tempo.repository.QueueRepository
|
||||
import com.cappielloantonio.tempo.ui.activity.MainActivity
|
||||
import com.cappielloantonio.tempo.util.*
|
||||
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
|
||||
|
||||
@UnstableApi
|
||||
open class BaseMediaService : MediaLibraryService() {
|
||||
companion object {
|
||||
private const val CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON =
|
||||
"android.media3.session.demo.SHUFFLE_ON"
|
||||
private const val CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF =
|
||||
"android.media3.session.demo.SHUFFLE_OFF"
|
||||
private const val CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_OFF =
|
||||
"android.media3.session.demo.REPEAT_OFF"
|
||||
private const val CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ONE =
|
||||
"android.media3.session.demo.REPEAT_ONE"
|
||||
private const val CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ALL =
|
||||
"android.media3.session.demo.REPEAT_ALL"
|
||||
const val ACTION_BIND_EQUALIZER = "com.cappielloantonio.tempo.service.BIND_EQUALIZER"
|
||||
const val ACTION_EQUALIZER_UPDATED = "com.cappielloantonio.tempo.service.EQUALIZER_UPDATED"
|
||||
}
|
||||
|
||||
protected lateinit var exoplayer: ExoPlayer
|
||||
protected lateinit var mediaLibrarySession: MediaLibrarySession
|
||||
private lateinit var networkCallback: CustomNetworkCallback
|
||||
private lateinit var equalizerManager: EqualizerManager
|
||||
private val widgetUpdateHandler = Handler(Looper.getMainLooper())
|
||||
private var widgetUpdateScheduled = false
|
||||
private val widgetUpdateRunnable = object : Runnable {
|
||||
override fun run() {
|
||||
val player = mediaLibrarySession.player
|
||||
if (!player.isPlaying) {
|
||||
widgetUpdateScheduled = false
|
||||
return
|
||||
}
|
||||
updateWidget(player)
|
||||
widgetUpdateHandler.postDelayed(this, WIDGET_UPDATE_INTERVAL_MS)
|
||||
}
|
||||
}
|
||||
|
||||
private val binder = LocalBinder()
|
||||
|
||||
open fun playerInitHook() {
|
||||
initializeExoPlayer()
|
||||
initializeMediaLibrarySession(exoplayer)
|
||||
initializePlayerListener(exoplayer)
|
||||
setPlayer(null, exoplayer)
|
||||
}
|
||||
|
||||
open fun getMediaLibrarySessionCallback(): MediaLibrarySession.Callback {
|
||||
return CustomMediaLibrarySessionCallback(baseContext)
|
||||
}
|
||||
|
||||
fun updateMediaItems(player: Player) {
|
||||
Log.d(javaClass.toString(), "update items")
|
||||
val n = player.mediaItemCount
|
||||
val k = player.currentMediaItemIndex
|
||||
val current = player.currentPosition
|
||||
val items = (0..n - 1).map { MappingUtil.mapMediaItem(player.getMediaItemAt(it)) }
|
||||
player.clearMediaItems()
|
||||
player.setMediaItems(items, k, current)
|
||||
}
|
||||
|
||||
fun restorePlayerFromQueue(player: Player) {
|
||||
if (player.mediaItemCount > 0) return
|
||||
|
||||
val queueRepository = QueueRepository()
|
||||
val storedQueue = queueRepository.media
|
||||
if (storedQueue.isNullOrEmpty()) return
|
||||
|
||||
val mediaItems = MappingUtil.mapMediaItems(storedQueue)
|
||||
if (mediaItems.isEmpty()) return
|
||||
|
||||
val lastIndex = try {
|
||||
queueRepository.lastPlayedMediaIndex
|
||||
} catch (_: Exception) {
|
||||
0
|
||||
}.coerceIn(0, mediaItems.size - 1)
|
||||
|
||||
val lastPosition = try {
|
||||
queueRepository.lastPlayedMediaTimestamp
|
||||
} catch (_: Exception) {
|
||||
0L
|
||||
}.let { if (it < 0L) 0L else it }
|
||||
|
||||
player.setMediaItems(mediaItems, lastIndex, lastPosition)
|
||||
player.prepare()
|
||||
updateWidget(player)
|
||||
}
|
||||
|
||||
fun initializePlayerListener(player: Player) {
|
||||
player.addListener(object : Player.Listener {
|
||||
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
|
||||
Log.d(javaClass.toString(), "onMediaItemTransition" + player.currentMediaItemIndex)
|
||||
if (mediaItem == null) return
|
||||
|
||||
if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_SEEK || reason == Player.MEDIA_ITEM_TRANSITION_REASON_AUTO) {
|
||||
MediaManager.setLastPlayedTimestamp(mediaItem)
|
||||
}
|
||||
updateWidget(player)
|
||||
}
|
||||
|
||||
override fun onTracksChanged(tracks: Tracks) {
|
||||
Log.d(javaClass.toString(), "onTracksChanged " + player.currentMediaItemIndex)
|
||||
ReplayGainUtil.setReplayGain(player, tracks)
|
||||
val currentMediaItem = player.currentMediaItem
|
||||
if (currentMediaItem != null) {
|
||||
val item = MappingUtil.mapMediaItem(currentMediaItem)
|
||||
if (item.mediaMetadata.extras != null)
|
||||
MediaManager.scrobble(item, false)
|
||||
|
||||
if (player.nextMediaItemIndex == C.INDEX_UNSET)
|
||||
MediaManager.continuousPlay(player.currentMediaItem)
|
||||
}
|
||||
|
||||
if (player is ExoPlayer) {
|
||||
// https://stackoverflow.com/questions/56937283/exoplayer-shuffle-doesnt-reproduce-all-the-songs
|
||||
if (MediaManager.justStarted.get()) {
|
||||
Log.d(javaClass.toString(), "update shuffle order")
|
||||
MediaManager.justStarted.set(false)
|
||||
val shuffledList = IntArray(player.mediaItemCount) { i -> i }
|
||||
shuffledList.shuffle()
|
||||
val index = shuffledList.indexOf(player.currentMediaItemIndex)
|
||||
// swap current media index to the first index
|
||||
if (index > -1 && shuffledList.isNotEmpty()) {
|
||||
val tmp = shuffledList[0]
|
||||
shuffledList[0] = shuffledList[index]
|
||||
shuffledList[index] = tmp
|
||||
}
|
||||
player.shuffleOrder =
|
||||
DefaultShuffleOrder(shuffledList, kotlin.random.Random.nextLong())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onIsPlayingChanged(isPlaying: Boolean) {
|
||||
Log.d(javaClass.toString(), "onIsPlayingChanged " + player.currentMediaItemIndex)
|
||||
if (!isPlaying) {
|
||||
MediaManager.setPlayingPausedTimestamp(
|
||||
player.currentMediaItem,
|
||||
player.currentPosition
|
||||
)
|
||||
} else {
|
||||
MediaManager.scrobble(player.currentMediaItem, false)
|
||||
}
|
||||
if (isPlaying) {
|
||||
scheduleWidgetUpdates()
|
||||
} else {
|
||||
stopWidgetUpdates()
|
||||
}
|
||||
updateWidget(player)
|
||||
}
|
||||
|
||||
override fun onPlaybackStateChanged(playbackState: Int) {
|
||||
Log.d(javaClass.toString(), "onPlaybackStateChanged")
|
||||
super.onPlaybackStateChanged(playbackState)
|
||||
if (!player.hasNextMediaItem() &&
|
||||
playbackState == Player.STATE_ENDED &&
|
||||
player.mediaMetadata.extras?.getString("type") == Constants.MEDIA_TYPE_MUSIC
|
||||
) {
|
||||
MediaManager.scrobble(player.currentMediaItem, true)
|
||||
MediaManager.saveChronology(player.currentMediaItem)
|
||||
}
|
||||
updateWidget(player)
|
||||
}
|
||||
|
||||
override fun onPositionDiscontinuity(
|
||||
oldPosition: Player.PositionInfo,
|
||||
newPosition: Player.PositionInfo,
|
||||
reason: Int
|
||||
) {
|
||||
Log.d(javaClass.toString(), "onPositionDiscontinuity")
|
||||
super.onPositionDiscontinuity(oldPosition, newPosition, reason)
|
||||
|
||||
if (reason == Player.DISCONTINUITY_REASON_AUTO_TRANSITION) {
|
||||
if (oldPosition.mediaItem?.mediaMetadata?.extras?.getString("type") == Constants.MEDIA_TYPE_MUSIC) {
|
||||
MediaManager.scrobble(oldPosition.mediaItem, true)
|
||||
MediaManager.saveChronology(oldPosition.mediaItem)
|
||||
}
|
||||
|
||||
if (newPosition.mediaItem?.mediaMetadata?.extras?.getString("type") == Constants.MEDIA_TYPE_MUSIC) {
|
||||
MediaManager.setLastPlayedTimestamp(newPosition.mediaItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onShuffleModeEnabledChanged(shuffleModeEnabled: Boolean) {
|
||||
Preferences.setShuffleModeEnabled(shuffleModeEnabled)
|
||||
}
|
||||
|
||||
override fun onRepeatModeChanged(repeatMode: Int) {
|
||||
Preferences.setRepeatMode(repeatMode)
|
||||
}
|
||||
|
||||
override fun onAudioSessionIdChanged(audioSessionId: Int) {
|
||||
Log.d(javaClass.toString(), "onAudioSessionIdChanged")
|
||||
attachEqualizerIfPossible(audioSessionId)
|
||||
}
|
||||
})
|
||||
if (player.isPlaying) {
|
||||
scheduleWidgetUpdates()
|
||||
}
|
||||
}
|
||||
|
||||
fun setPlayer(oldPlayer: Player?, newPlayer: Player) {
|
||||
if (oldPlayer === newPlayer) return
|
||||
if (oldPlayer != null) {
|
||||
val currentQueue = getQueueFromPlayer(oldPlayer)
|
||||
val currentIndex = oldPlayer.currentMediaItemIndex
|
||||
val currentPosition = oldPlayer.currentPosition
|
||||
val isPlaying = oldPlayer.playWhenReady
|
||||
oldPlayer.stop()
|
||||
newPlayer.setMediaItems(currentQueue, currentIndex, currentPosition)
|
||||
newPlayer.playWhenReady = isPlaying
|
||||
newPlayer.prepare()
|
||||
}
|
||||
mediaLibrarySession.player = newPlayer
|
||||
}
|
||||
|
||||
open fun releasePlayers() {
|
||||
exoplayer.release()
|
||||
}
|
||||
|
||||
fun getQueueFromPlayer(player: Player): List<MediaItem> {
|
||||
return (0..player.mediaItemCount - 1).map(player::getMediaItemAt)
|
||||
}
|
||||
|
||||
override fun onTaskRemoved(rootIntent: Intent?) {
|
||||
val player = mediaLibrarySession.player
|
||||
|
||||
if (!player.playWhenReady || player.mediaItemCount == 0) {
|
||||
stopSelf()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
playerInitHook()
|
||||
initializeEqualizerManager()
|
||||
initializeNetworkListener()
|
||||
restorePlayerFromQueue(mediaLibrarySession.player)
|
||||
}
|
||||
|
||||
override fun onGetSession(controllerInfo: ControllerInfo): MediaLibrarySession {
|
||||
return mediaLibrarySession
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
releaseNetworkCallback()
|
||||
equalizerManager.release()
|
||||
stopWidgetUpdates()
|
||||
releasePlayers()
|
||||
mediaLibrarySession.release()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? {
|
||||
// Check if the intent is for our custom equalizer binder
|
||||
if (intent?.action == ACTION_BIND_EQUALIZER) {
|
||||
return binder
|
||||
}
|
||||
// Otherwise, handle it as a normal MediaLibraryService connection
|
||||
return super.onBind(intent)
|
||||
}
|
||||
|
||||
private fun initializeExoPlayer() {
|
||||
exoplayer = ExoPlayer.Builder(this)
|
||||
.setRenderersFactory(getRenderersFactory())
|
||||
.setMediaSourceFactory(getMediaSourceFactory())
|
||||
.setAudioAttributes(AudioAttributes.DEFAULT, true)
|
||||
.setHandleAudioBecomingNoisy(true)
|
||||
.setWakeMode(C.WAKE_MODE_NETWORK)
|
||||
.setLoadControl(initializeLoadControl())
|
||||
.build()
|
||||
|
||||
exoplayer.shuffleModeEnabled = Preferences.isShuffleModeEnabled()
|
||||
exoplayer.repeatMode = Preferences.getRepeatMode()
|
||||
}
|
||||
|
||||
private fun initializeEqualizerManager() {
|
||||
equalizerManager = EqualizerManager()
|
||||
val audioSessionId = exoplayer.audioSessionId
|
||||
attachEqualizerIfPossible(audioSessionId)
|
||||
}
|
||||
|
||||
private fun initializeMediaLibrarySession(player: Player) {
|
||||
Log.d(javaClass.toString(), "initializeMediaLibrarySession")
|
||||
val sessionActivityPendingIntent =
|
||||
TaskStackBuilder.create(this).run {
|
||||
addNextIntent(Intent(baseContext, MainActivity::class.java))
|
||||
getPendingIntent(0, FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT)
|
||||
}
|
||||
|
||||
mediaLibrarySession =
|
||||
MediaLibrarySession.Builder(this, player, getMediaLibrarySessionCallback())
|
||||
.setSessionActivity(sessionActivityPendingIntent)
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun initializeNetworkListener() {
|
||||
networkCallback = CustomNetworkCallback()
|
||||
getSystemService(ConnectivityManager::class.java).registerDefaultNetworkCallback(
|
||||
networkCallback
|
||||
)
|
||||
updateMediaItems(mediaLibrarySession.player)
|
||||
}
|
||||
|
||||
private fun initializeLoadControl(): DefaultLoadControl {
|
||||
return DefaultLoadControl.Builder()
|
||||
.setBufferDurationsMs(
|
||||
(DefaultLoadControl.DEFAULT_MIN_BUFFER_MS * Preferences.getBufferingStrategy()).toInt(),
|
||||
(DefaultLoadControl.DEFAULT_MAX_BUFFER_MS * Preferences.getBufferingStrategy()).toInt(),
|
||||
DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS,
|
||||
DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS
|
||||
)
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun releaseNetworkCallback() {
|
||||
getSystemService(ConnectivityManager::class.java).unregisterNetworkCallback(networkCallback)
|
||||
}
|
||||
|
||||
private fun updateWidget(player: Player) {
|
||||
val mi = player.currentMediaItem
|
||||
val title = mi?.mediaMetadata?.title?.toString()
|
||||
?: mi?.mediaMetadata?.extras?.getString("title")
|
||||
val artist = mi?.mediaMetadata?.artist?.toString()
|
||||
?: mi?.mediaMetadata?.extras?.getString("artist")
|
||||
val album = mi?.mediaMetadata?.albumTitle?.toString()
|
||||
?: mi?.mediaMetadata?.extras?.getString("album")
|
||||
val extras = mi?.mediaMetadata?.extras
|
||||
val coverId = extras?.getString("coverArtId")
|
||||
val songLink = extras?.getString("assetLinkSong")
|
||||
?: AssetLinkUtil.buildLink(AssetLinkUtil.TYPE_SONG, extras?.getString("id"))
|
||||
val albumLink = extras?.getString("assetLinkAlbum")
|
||||
?: AssetLinkUtil.buildLink(AssetLinkUtil.TYPE_ALBUM, extras?.getString("albumId"))
|
||||
val artistLink = extras?.getString("assetLinkArtist")
|
||||
?: AssetLinkUtil.buildLink(AssetLinkUtil.TYPE_ARTIST, extras?.getString("artistId"))
|
||||
val position = player.currentPosition.takeIf { it != C.TIME_UNSET } ?: 0L
|
||||
val duration = player.duration.takeIf { it != C.TIME_UNSET } ?: 0L
|
||||
WidgetUpdateManager.updateFromState(
|
||||
this,
|
||||
title ?: "",
|
||||
artist ?: "",
|
||||
album ?: "",
|
||||
coverId,
|
||||
player.isPlaying,
|
||||
player.shuffleModeEnabled,
|
||||
player.repeatMode,
|
||||
position,
|
||||
duration,
|
||||
songLink,
|
||||
albumLink,
|
||||
artistLink
|
||||
)
|
||||
}
|
||||
|
||||
private fun scheduleWidgetUpdates() {
|
||||
if (widgetUpdateScheduled) return
|
||||
widgetUpdateHandler.postDelayed(widgetUpdateRunnable, WIDGET_UPDATE_INTERVAL_MS)
|
||||
widgetUpdateScheduled = true
|
||||
}
|
||||
|
||||
private fun stopWidgetUpdates() {
|
||||
if (!widgetUpdateScheduled) return
|
||||
widgetUpdateHandler.removeCallbacks(widgetUpdateRunnable)
|
||||
widgetUpdateScheduled = false
|
||||
}
|
||||
|
||||
private fun attachEqualizerIfPossible(audioSessionId: Int): Boolean {
|
||||
if (audioSessionId == 0 || audioSessionId == -1) return false
|
||||
val attached = equalizerManager.attachToSession(audioSessionId)
|
||||
if (attached) {
|
||||
val enabled = Preferences.isEqualizerEnabled()
|
||||
equalizerManager.setEnabled(enabled)
|
||||
val bands = equalizerManager.getNumberOfBands()
|
||||
val savedLevels = Preferences.getEqualizerBandLevels(bands)
|
||||
for (i in 0 until bands) {
|
||||
equalizerManager.setBandLevel(i.toShort(), savedLevels[i])
|
||||
}
|
||||
sendBroadcast(Intent(ACTION_EQUALIZER_UPDATED))
|
||||
}
|
||||
return attached
|
||||
}
|
||||
|
||||
private fun getRenderersFactory() = DownloadUtil.buildRenderersFactory(this, false)
|
||||
|
||||
private fun getMediaSourceFactory(): MediaSource.Factory = DynamicMediaSourceFactory(this)
|
||||
|
||||
@UnstableApi
|
||||
private class CustomMediaLibrarySessionCallback : MediaLibrarySession.Callback {
|
||||
private val shuffleCommands: List<CommandButton>
|
||||
private val repeatCommands: List<CommandButton>
|
||||
|
||||
constructor(ctx: Context) {
|
||||
shuffleCommands = listOf(
|
||||
CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON,
|
||||
CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF
|
||||
)
|
||||
.map { getShuffleCommandButton(SessionCommand(it, Bundle.EMPTY), ctx) }
|
||||
repeatCommands = listOf(
|
||||
CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_OFF,
|
||||
CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ONE,
|
||||
CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ALL
|
||||
)
|
||||
.map { getRepeatCommandButton(SessionCommand(it, Bundle.EMPTY), ctx) }
|
||||
}
|
||||
|
||||
override fun onConnect(
|
||||
session: MediaSession,
|
||||
controller: ControllerInfo
|
||||
): MediaSession.ConnectionResult {
|
||||
val connectionResult = super.onConnect(session, controller)
|
||||
val availableSessionCommands = connectionResult.availableSessionCommands.buildUpon()
|
||||
|
||||
(shuffleCommands + repeatCommands).forEach { commandButton ->
|
||||
commandButton.sessionCommand?.let { availableSessionCommands.add(it) }
|
||||
}
|
||||
|
||||
val result = MediaSession.ConnectionResult.AcceptedResultBuilder(session)
|
||||
.setAvailableSessionCommands(availableSessionCommands.build())
|
||||
.setAvailablePlayerCommands(connectionResult.availablePlayerCommands)
|
||||
.setMediaButtonPreferences(buildCustomLayout(session.player))
|
||||
.build()
|
||||
return result
|
||||
}
|
||||
|
||||
override fun onCustomCommand(
|
||||
session: MediaSession,
|
||||
controller: ControllerInfo,
|
||||
customCommand: SessionCommand,
|
||||
args: Bundle
|
||||
): ListenableFuture<SessionResult> {
|
||||
Log.d(javaClass.toString(), "onCustomCommand")
|
||||
when (customCommand.customAction) {
|
||||
CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON -> session.player.shuffleModeEnabled = true
|
||||
CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF -> session.player.shuffleModeEnabled = false
|
||||
CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_OFF,
|
||||
CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ALL,
|
||||
CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ONE -> {
|
||||
val nextMode = when (session.player.repeatMode) {
|
||||
Player.REPEAT_MODE_ONE -> Player.REPEAT_MODE_ALL
|
||||
Player.REPEAT_MODE_OFF -> Player.REPEAT_MODE_ONE
|
||||
else -> Player.REPEAT_MODE_OFF
|
||||
}
|
||||
session.player.repeatMode = nextMode
|
||||
}
|
||||
}
|
||||
|
||||
session.setMediaButtonPreferences(buildCustomLayout(session.player))
|
||||
return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
|
||||
}
|
||||
|
||||
override fun onAddMediaItems(
|
||||
mediaSession: MediaSession,
|
||||
controller: ControllerInfo,
|
||||
mediaItems: List<MediaItem>
|
||||
): ListenableFuture<List<MediaItem>> {
|
||||
Log.d(javaClass.toString(), "onAddMediaItems")
|
||||
val updatedMediaItems = mediaItems.map { mediaItem ->
|
||||
val mediaMetadata = mediaItem.mediaMetadata
|
||||
val newMetadata = mediaMetadata.buildUpon()
|
||||
.setArtist(
|
||||
if (mediaMetadata.artist != null) mediaMetadata.artist
|
||||
else mediaMetadata.extras?.getString("uri") ?: ""
|
||||
)
|
||||
.build()
|
||||
|
||||
mediaItem.buildUpon()
|
||||
.setUri(mediaItem.requestMetadata.mediaUri)
|
||||
.setMediaMetadata(newMetadata)
|
||||
.setMimeType(MimeTypes.BASE_TYPE_AUDIO)
|
||||
.build()
|
||||
}
|
||||
return Futures.immediateFuture(updatedMediaItems)
|
||||
}
|
||||
|
||||
@SuppressLint("PrivateResource")
|
||||
private fun getShuffleCommandButton(
|
||||
sessionCommand: SessionCommand,
|
||||
ctx: Context
|
||||
): CommandButton {
|
||||
val isOn = sessionCommand.customAction == CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON
|
||||
return CommandButton.Builder(if (isOn) CommandButton.ICON_SHUFFLE_OFF else CommandButton.ICON_SHUFFLE_ON)
|
||||
.setSessionCommand(sessionCommand)
|
||||
.setDisplayName(
|
||||
ctx.getString(
|
||||
if (isOn) R.string.exo_controls_shuffle_on_description
|
||||
else R.string.exo_controls_shuffle_off_description
|
||||
)
|
||||
)
|
||||
.build()
|
||||
}
|
||||
|
||||
@SuppressLint("PrivateResource")
|
||||
private fun getRepeatCommandButton(
|
||||
sessionCommand: SessionCommand,
|
||||
ctx: Context
|
||||
): CommandButton {
|
||||
val icon = when (sessionCommand.customAction) {
|
||||
CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ONE -> CommandButton.ICON_REPEAT_ONE
|
||||
CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ALL -> CommandButton.ICON_REPEAT_ALL
|
||||
else -> CommandButton.ICON_REPEAT_OFF
|
||||
}
|
||||
val description = when (sessionCommand.customAction) {
|
||||
CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ONE -> R.string.exo_controls_repeat_one_description
|
||||
CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ALL -> R.string.exo_controls_repeat_all_description
|
||||
else -> R.string.exo_controls_repeat_off_description
|
||||
}
|
||||
return CommandButton.Builder(icon)
|
||||
.setSessionCommand(sessionCommand)
|
||||
.setDisplayName(ctx.getString(description))
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun buildCustomLayout(player: Player): ImmutableList<CommandButton> {
|
||||
val shuffle = shuffleCommands[if (player.shuffleModeEnabled) 1 else 0]
|
||||
val repeat = when (player.repeatMode) {
|
||||
Player.REPEAT_MODE_ONE -> repeatCommands[1]
|
||||
Player.REPEAT_MODE_ALL -> repeatCommands[2]
|
||||
else -> repeatCommands[0]
|
||||
}
|
||||
return ImmutableList.of(shuffle, repeat)
|
||||
}
|
||||
}
|
||||
|
||||
private inner class CustomNetworkCallback : ConnectivityManager.NetworkCallback() {
|
||||
var wasWifi = false
|
||||
|
||||
init {
|
||||
val manager = getSystemService(ConnectivityManager::class.java)
|
||||
val network = manager.activeNetwork
|
||||
val capabilities = manager.getNetworkCapabilities(network)
|
||||
if (capabilities != null)
|
||||
wasWifi = capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
|
||||
}
|
||||
|
||||
override fun onCapabilitiesChanged(
|
||||
network: Network,
|
||||
networkCapabilities: NetworkCapabilities
|
||||
) {
|
||||
val isWifi = networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
|
||||
if (isWifi != wasWifi) {
|
||||
wasWifi = isWifi
|
||||
widgetUpdateHandler.post {
|
||||
updateMediaItems(mediaLibrarySession.player)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inner class LocalBinder : Binder() {
|
||||
fun getEqualizerManager(): EqualizerManager {
|
||||
return equalizerManager
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private const val WIDGET_UPDATE_INTERVAL_MS = 1000L
|
||||
|
||||
|
|
@ -36,10 +36,12 @@ import com.google.common.util.concurrent.MoreExecutors;
|
|||
import java.lang.ref.WeakReference;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public class MediaManager {
|
||||
private static final String TAG = "MediaManager";
|
||||
private static WeakReference<MediaBrowser> attachedBrowserRef = new WeakReference<>(null);
|
||||
public static AtomicBoolean justStarted = new AtomicBoolean(false);
|
||||
|
||||
public static void registerPlaybackObserver(
|
||||
ListenableFuture<MediaBrowser> browserFuture,
|
||||
|
|
@ -179,8 +181,8 @@ public class MediaManager {
|
|||
try {
|
||||
if (mediaBrowserListenableFuture.isDone()) {
|
||||
MediaBrowser browser = mediaBrowserListenableFuture.get();
|
||||
browser.clearMediaItems();
|
||||
browser.setMediaItems(MappingUtil.mapMediaItems(media));
|
||||
justStarted.set(true);
|
||||
browser.setMediaItems(MappingUtil.mapMediaItems(media), startIndex, 0);
|
||||
browser.prepare();
|
||||
|
||||
Player.Listener timelineListener = new Player.Listener() {
|
||||
|
|
@ -210,10 +212,11 @@ public class MediaManager {
|
|||
mediaBrowserListenableFuture.addListener(() -> {
|
||||
try {
|
||||
if (mediaBrowserListenableFuture.isDone()) {
|
||||
mediaBrowserListenableFuture.get().clearMediaItems();
|
||||
mediaBrowserListenableFuture.get().setMediaItem(MappingUtil.mapMediaItem(media));
|
||||
mediaBrowserListenableFuture.get().prepare();
|
||||
mediaBrowserListenableFuture.get().play();
|
||||
MediaBrowser browser = mediaBrowserListenableFuture.get();
|
||||
justStarted.set(true);
|
||||
browser.setMediaItem(MappingUtil.mapMediaItem(media));
|
||||
browser.prepare();
|
||||
browser.play();
|
||||
enqueueDatabase(media, true, 0);
|
||||
}
|
||||
} catch (ExecutionException | InterruptedException e) {
|
||||
|
|
@ -229,7 +232,7 @@ public class MediaManager {
|
|||
try {
|
||||
if (mediaBrowserListenableFuture.isDone()) {
|
||||
MediaBrowser mediaBrowser = mediaBrowserListenableFuture.get();
|
||||
mediaBrowser.clearMediaItems();
|
||||
justStarted.set(true);
|
||||
mediaBrowser.setMediaItem(mediaItem);
|
||||
mediaBrowser.prepare();
|
||||
mediaBrowser.play();
|
||||
|
|
@ -247,10 +250,11 @@ public class MediaManager {
|
|||
mediaBrowserListenableFuture.addListener(() -> {
|
||||
try {
|
||||
if (mediaBrowserListenableFuture.isDone()) {
|
||||
mediaBrowserListenableFuture.get().clearMediaItems();
|
||||
mediaBrowserListenableFuture.get().setMediaItem(MappingUtil.mapInternetRadioStation(internetRadioStation));
|
||||
mediaBrowserListenableFuture.get().prepare();
|
||||
mediaBrowserListenableFuture.get().play();
|
||||
MediaBrowser browser = mediaBrowserListenableFuture.get();
|
||||
justStarted.set(true);
|
||||
browser.setMediaItem(MappingUtil.mapInternetRadioStation(internetRadioStation));
|
||||
browser.prepare();
|
||||
browser.play();
|
||||
}
|
||||
} catch (ExecutionException | InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
|
|
@ -264,10 +268,11 @@ public class MediaManager {
|
|||
mediaBrowserListenableFuture.addListener(() -> {
|
||||
try {
|
||||
if (mediaBrowserListenableFuture.isDone()) {
|
||||
mediaBrowserListenableFuture.get().clearMediaItems();
|
||||
mediaBrowserListenableFuture.get().setMediaItem(MappingUtil.mapMediaItem(podcastEpisode));
|
||||
mediaBrowserListenableFuture.get().prepare();
|
||||
mediaBrowserListenableFuture.get().play();
|
||||
MediaBrowser browser = mediaBrowserListenableFuture.get();
|
||||
justStarted.set(true);
|
||||
browser.setMediaItem(MappingUtil.mapMediaItem(podcastEpisode));
|
||||
browser.prepare();
|
||||
browser.play();
|
||||
}
|
||||
} catch (ExecutionException | InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
|
|
@ -281,9 +286,11 @@ public class MediaManager {
|
|||
mediaBrowserListenableFuture.addListener(() -> {
|
||||
try {
|
||||
if (mediaBrowserListenableFuture.isDone()) {
|
||||
if (playImmediatelyAfter && mediaBrowserListenableFuture.get().getNextMediaItemIndex() != -1) {
|
||||
enqueueDatabase(media, false, mediaBrowserListenableFuture.get().getNextMediaItemIndex());
|
||||
mediaBrowserListenableFuture.get().addMediaItems(mediaBrowserListenableFuture.get().getNextMediaItemIndex(), MappingUtil.mapMediaItems(media));
|
||||
Log.e(TAG, "enqueue");
|
||||
MediaBrowser browser = mediaBrowserListenableFuture.get();
|
||||
if (playImmediatelyAfter && browser.getNextMediaItemIndex() != -1) {
|
||||
enqueueDatabase(media, false, browser.getNextMediaItemIndex());
|
||||
browser.addMediaItems(browser.getNextMediaItemIndex(), MappingUtil.mapMediaItems(media));
|
||||
} else {
|
||||
enqueueDatabase(media, false, mediaBrowserListenableFuture.get().getMediaItemCount());
|
||||
mediaBrowserListenableFuture.get().addMediaItems(MappingUtil.mapMediaItems(media));
|
||||
|
|
@ -301,9 +308,11 @@ public class MediaManager {
|
|||
mediaBrowserListenableFuture.addListener(() -> {
|
||||
try {
|
||||
if (mediaBrowserListenableFuture.isDone()) {
|
||||
if (playImmediatelyAfter && mediaBrowserListenableFuture.get().getNextMediaItemIndex() != -1) {
|
||||
enqueueDatabase(media, false, mediaBrowserListenableFuture.get().getNextMediaItemIndex());
|
||||
mediaBrowserListenableFuture.get().addMediaItem(mediaBrowserListenableFuture.get().getNextMediaItemIndex(), MappingUtil.mapMediaItem(media));
|
||||
Log.e(TAG, "enqueue");
|
||||
MediaBrowser browser = mediaBrowserListenableFuture.get();
|
||||
if (playImmediatelyAfter && browser.getNextMediaItemIndex() != -1) {
|
||||
enqueueDatabase(media, false, browser.getNextMediaItemIndex());
|
||||
browser.addMediaItem(browser.getNextMediaItemIndex(), MappingUtil.mapMediaItem(media));
|
||||
} else {
|
||||
enqueueDatabase(media, false, mediaBrowserListenableFuture.get().getMediaItemCount());
|
||||
mediaBrowserListenableFuture.get().addMediaItem(MappingUtil.mapMediaItem(media));
|
||||
|
|
@ -321,8 +330,10 @@ public class MediaManager {
|
|||
mediaBrowserListenableFuture.addListener(() -> {
|
||||
try {
|
||||
if (mediaBrowserListenableFuture.isDone()) {
|
||||
mediaBrowserListenableFuture.get().removeMediaItems(startIndex, endIndex + 1);
|
||||
mediaBrowserListenableFuture.get().addMediaItems(MappingUtil.mapMediaItems(media).subList(startIndex, endIndex + 1));
|
||||
Log.e(TAG, "shuffle");
|
||||
MediaBrowser browser = mediaBrowserListenableFuture.get();
|
||||
browser.removeMediaItems(startIndex, endIndex + 1);
|
||||
browser.addMediaItems(MappingUtil.mapMediaItems(media).subList(startIndex, endIndex + 1));
|
||||
swapDatabase(media);
|
||||
}
|
||||
} catch (ExecutionException | InterruptedException e) {
|
||||
|
|
@ -337,6 +348,7 @@ public class MediaManager {
|
|||
mediaBrowserListenableFuture.addListener(() -> {
|
||||
try {
|
||||
if (mediaBrowserListenableFuture.isDone()) {
|
||||
Log.e(TAG, "swap");
|
||||
mediaBrowserListenableFuture.get().moveMediaItem(from, to);
|
||||
swapDatabase(media);
|
||||
}
|
||||
|
|
@ -352,6 +364,7 @@ public class MediaManager {
|
|||
mediaBrowserListenableFuture.addListener(() -> {
|
||||
try {
|
||||
if (mediaBrowserListenableFuture.isDone()) {
|
||||
Log.e(TAG, "remove");
|
||||
if (mediaBrowserListenableFuture.get().getMediaItemCount() > 1 && mediaBrowserListenableFuture.get().getCurrentMediaItemIndex() != toRemove) {
|
||||
mediaBrowserListenableFuture.get().removeMediaItem(toRemove);
|
||||
removeDatabase(media, toRemove);
|
||||
|
|
@ -371,6 +384,7 @@ public class MediaManager {
|
|||
mediaBrowserListenableFuture.addListener(() -> {
|
||||
try {
|
||||
if (mediaBrowserListenableFuture.isDone()) {
|
||||
Log.e(TAG, "remove range");
|
||||
mediaBrowserListenableFuture.get().removeMediaItems(fromItem, toItem);
|
||||
removeRangeDatabase(media, fromItem, toItem);
|
||||
}
|
||||
|
|
@ -420,6 +434,7 @@ public class MediaManager {
|
|||
@Override
|
||||
public void onChanged(List<Child> media) {
|
||||
if (media != null) {
|
||||
Log.e(TAG, "continuous play");
|
||||
ListenableFuture<MediaBrowser> mediaBrowserListenableFuture = new MediaBrowser.Builder(
|
||||
App.getContext(),
|
||||
new SessionToken(App.getContext(), new ComponentName(App.getContext(), MediaService.class))
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import androidx.fragment.app.Fragment
|
|||
import androidx.media3.common.util.UnstableApi
|
||||
import com.cappielloantonio.tempo.R
|
||||
import com.cappielloantonio.tempo.service.EqualizerManager
|
||||
import com.cappielloantonio.tempo.service.BaseMediaService
|
||||
import com.cappielloantonio.tempo.service.MediaService
|
||||
import com.cappielloantonio.tempo.util.Preferences
|
||||
|
||||
|
|
@ -35,7 +36,7 @@ class EqualizerFragment : Fragment() {
|
|||
private val equalizerUpdatedReceiver = object : BroadcastReceiver() {
|
||||
@OptIn(UnstableApi::class)
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
if (intent?.action == MediaService.ACTION_EQUALIZER_UPDATED) {
|
||||
if (intent?.action == BaseMediaService.ACTION_EQUALIZER_UPDATED) {
|
||||
initUI()
|
||||
restoreEqualizerPreferences()
|
||||
}
|
||||
|
|
@ -45,7 +46,7 @@ class EqualizerFragment : Fragment() {
|
|||
private val connection = object : ServiceConnection {
|
||||
@OptIn(UnstableApi::class)
|
||||
override fun onServiceConnected(className: ComponentName, service: IBinder) {
|
||||
val binder = service as MediaService.LocalBinder
|
||||
val binder = service as BaseMediaService.LocalBinder
|
||||
equalizerManager = binder.getEqualizerManager()
|
||||
initUI()
|
||||
restoreEqualizerPreferences()
|
||||
|
|
@ -60,14 +61,14 @@ class EqualizerFragment : Fragment() {
|
|||
override fun onStart() {
|
||||
super.onStart()
|
||||
Intent(requireContext(), MediaService::class.java).also { intent ->
|
||||
intent.action = MediaService.ACTION_BIND_EQUALIZER
|
||||
intent.action = BaseMediaService.ACTION_BIND_EQUALIZER
|
||||
requireActivity().bindService(intent, connection, Context.BIND_AUTO_CREATE)
|
||||
}
|
||||
if (!receiverRegistered) {
|
||||
ContextCompat.registerReceiver(
|
||||
requireContext(),
|
||||
equalizerUpdatedReceiver,
|
||||
IntentFilter(MediaService.ACTION_EQUALIZER_UPDATED),
|
||||
IntentFilter(BaseMediaService.ACTION_EQUALIZER_UPDATED),
|
||||
ContextCompat.RECEIVER_NOT_EXPORTED
|
||||
)
|
||||
receiverRegistered = true
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
package com.cappielloantonio.tempo.util;
|
||||
|
||||
import androidx.annotation.OptIn;
|
||||
import androidx.media3.common.C;
|
||||
import androidx.media3.common.MediaItem;
|
||||
import androidx.media3.common.Metadata;
|
||||
import androidx.media3.common.Tracks;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.exoplayer.ExoPlayer;
|
||||
import androidx.media3.common.Player;
|
||||
|
||||
import com.cappielloantonio.tempo.model.ReplayGain;
|
||||
|
||||
|
|
@ -17,7 +18,7 @@ import java.util.Objects;
|
|||
public class ReplayGainUtil {
|
||||
private static final String[] tags = {"REPLAYGAIN_TRACK_GAIN", "REPLAYGAIN_ALBUM_GAIN", "R128_TRACK_GAIN", "R128_ALBUM_GAIN"};
|
||||
|
||||
public static void setReplayGain(ExoPlayer player, Tracks tracks) {
|
||||
public static void setReplayGain(Player player, Tracks tracks) {
|
||||
List<Metadata> metadata = getMetadata(tracks);
|
||||
List<ReplayGain> gains = getReplayGains(metadata);
|
||||
|
||||
|
|
@ -62,7 +63,7 @@ public class ReplayGainUtil {
|
|||
}
|
||||
}
|
||||
|
||||
if (gains.size() == 0) gains.add(0, new ReplayGain());
|
||||
if (gains.isEmpty()) gains.add(0, new ReplayGain());
|
||||
if (gains.size() == 1) gains.add(1, new ReplayGain());
|
||||
|
||||
return gains;
|
||||
|
|
@ -108,7 +109,7 @@ public class ReplayGainUtil {
|
|||
}
|
||||
}
|
||||
|
||||
private static void applyReplayGain(ExoPlayer player, List<ReplayGain> gains) {
|
||||
private static void applyReplayGain(Player player, List<ReplayGain> gains) {
|
||||
if (Objects.equals(Preferences.getReplayGainMode(), "disabled") || gains == null || gains.isEmpty()) {
|
||||
setNoReplayGain(player);
|
||||
return;
|
||||
|
|
@ -137,33 +138,33 @@ public class ReplayGainUtil {
|
|||
setNoReplayGain(player);
|
||||
}
|
||||
|
||||
private static void setNoReplayGain(ExoPlayer player) {
|
||||
private static void setNoReplayGain(Player player) {
|
||||
setReplayGain(player, 0f);
|
||||
}
|
||||
|
||||
private static void setTrackReplayGain(ExoPlayer player, List<ReplayGain> gains) {
|
||||
private static void setTrackReplayGain(Player player, List<ReplayGain> gains) {
|
||||
float trackGain = gains.get(0).getTrackGain() != 0f ? gains.get(0).getTrackGain() : gains.get(1).getTrackGain();
|
||||
|
||||
setReplayGain(player, trackGain != 0f ? trackGain : 0f);
|
||||
}
|
||||
|
||||
private static void setAlbumReplayGain(ExoPlayer player, List<ReplayGain> gains) {
|
||||
private static void setAlbumReplayGain(Player player, List<ReplayGain> gains) {
|
||||
float albumGain = gains.get(0).getAlbumGain() != 0f ? gains.get(0).getAlbumGain() : gains.get(1).getAlbumGain();
|
||||
|
||||
setReplayGain(player, albumGain != 0f ? albumGain : 0f);
|
||||
}
|
||||
|
||||
private static void setAutoReplayGain(ExoPlayer player, List<ReplayGain> gains) {
|
||||
private static void setAutoReplayGain(Player player, List<ReplayGain> gains) {
|
||||
float albumGain = gains.get(0).getAlbumGain() != 0f ? gains.get(0).getAlbumGain() : gains.get(1).getAlbumGain();
|
||||
float trackGain = gains.get(0).getTrackGain() != 0f ? gains.get(0).getTrackGain() : gains.get(1).getTrackGain();
|
||||
|
||||
setReplayGain(player, albumGain != 0f ? albumGain : trackGain);
|
||||
}
|
||||
|
||||
private static boolean areTracksConsecutive(ExoPlayer player) {
|
||||
private static boolean areTracksConsecutive(Player player) {
|
||||
MediaItem currentMediaItem = player.getCurrentMediaItem();
|
||||
int currentMediaItemIndex = player.getCurrentMediaItemIndex();
|
||||
MediaItem pastMediaItem = currentMediaItemIndex > 0 ? player.getMediaItemAt(currentMediaItemIndex - 1) : null;
|
||||
int prevMediaItemIndex = player.getPreviousMediaItemIndex();
|
||||
MediaItem pastMediaItem = prevMediaItemIndex == C.INDEX_UNSET ? null : player.getMediaItemAt(prevMediaItemIndex);
|
||||
|
||||
return currentMediaItem != null &&
|
||||
pastMediaItem != null &&
|
||||
|
|
@ -172,7 +173,7 @@ public class ReplayGainUtil {
|
|||
pastMediaItem.mediaMetadata.albumTitle.toString().equals(currentMediaItem.mediaMetadata.albumTitle.toString());
|
||||
}
|
||||
|
||||
private static void setReplayGain(ExoPlayer player, float gain) {
|
||||
private static void setReplayGain(Player player, float gain) {
|
||||
player.setVolume((float) Math.pow(10f, gain / 20f));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -177,6 +177,7 @@
|
|||
<string name="menu_filter_download">Descargado</string>
|
||||
<string name="menu_group_by_album">Álbum</string>
|
||||
<string name="menu_group_by_artist">Artista</string>
|
||||
<string name="settings_github_update_title">Comprobar actualizaciones en GitHub</string>
|
||||
<string name="settings_scan_result">Escaneo: hay %1$d pistas</string>
|
||||
<string name="settings_support_title">Soporte al usuario</string>
|
||||
<string name="settings_image_size">Resolución de la imagen</string>
|
||||
|
|
@ -185,7 +186,7 @@
|
|||
<string name="settings_logout_title">Cerrar sesión</string>
|
||||
<string name="settings_github_link">https://github.com/eddyizm/tempus</string>
|
||||
<string name="settings_github_summary">Siga el desarrollo</string>
|
||||
<string name="settings_github_title">Github</string>
|
||||
<string name="settings_github_title">GitHub</string>
|
||||
<string name="menu_group_by_genre">Género</string>
|
||||
<string name="menu_group_by_track">Pista</string>
|
||||
<string name="menu_group_by_year">Año</string>
|
||||
|
|
@ -199,6 +200,7 @@
|
|||
<string name="menu_sort_artist">Artista</string>
|
||||
<string name="menu_sort_name">Nombre</string>
|
||||
<string name="menu_sort_random">Aleatorio</string>
|
||||
<string name="menu_sort_album_count">Número de álbumes</string>
|
||||
<string name="menu_sort_recently_added">Añadido recientemente</string>
|
||||
<string name="menu_sort_recently_played">Reproducido recientemente</string>
|
||||
<string name="menu_sort_most_played">Lo más reproducido</string>
|
||||
|
|
@ -481,6 +483,7 @@
|
|||
<string name="player_lyrics_downloaded_content_description">Letras descargadas para uso sin conexión</string>
|
||||
<string name="player_lyrics_download_success">Letra guardada para uso sin conexión</string>
|
||||
<string name="settings_allow_playlist_duplicates">Permitir añadir pistas repetidas a la lista</string>
|
||||
<string name="settings_github_update_summary">Si se usa la versión de GitHub, la app comprobará nuevas actualizaciones del APK.</string>
|
||||
<string name="settings_support_summary">Participa en las discusiones y el soporte de la comunidad</string>
|
||||
<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>
|
||||
|
|
@ -505,4 +508,7 @@
|
|||
<string name="asset_link_error_album">No se ha podido abrir el álbum</string>
|
||||
<string name="asset_link_error_artist">No se ha podido abrir el artista</string>
|
||||
<string name="asset_link_error_playlist">No se ha podido abrir la lista de reproducción</string>
|
||||
<string name="settings_github_update">Actualizaciones</string>
|
||||
<string name="settings_artist_sort_by_album_count">Ordenar artistas por número de álbumes</string>
|
||||
<string name="settings_artist_sort_by_album_count_summary">Ordena los artistas por número de álbumes si la opción está habilitada. Si no, los ordena por nombre.</string>
|
||||
</resources>
|
||||
|
|
@ -254,4 +254,4 @@
|
|||
<item>3</item>
|
||||
<item>4</item>
|
||||
</string-array>
|
||||
</resources>
|
||||
</resources>
|
||||
|
|
@ -1,417 +1,536 @@
|
|||
<resources>
|
||||
<string name="activity_battery_optimizations_conclusion">Se hai problemi, visita https://dontkillmyapp.com. Qui trovi istruzioni dettagliate su come disabilitare le funzionalità di risparmio energetico che potrebbero influire sulle prestazioni dell\'app.</string>
|
||||
<string name="activity_battery_optimizations_summary">Per favore, disabilita le ottimizzazioni della batteria per la riproduzione multimediale quando lo schermo è spento.</string>
|
||||
<string name="activity_battery_optimizations_title">Ottimizzazioni della Batteria</string>
|
||||
<string name="activity_info_offline_mode">Modalità offline</string>
|
||||
<string name="activity_battery_optimizations_conclusion">Se hai problemi, visita https://dontkillmyapp.com. Qui trovi istruzioni dettagliate su come disabilitare le funzionalità di risparmio energetico che potrebbero influire sulle prestazioni dell\'app.</string>
|
||||
<string name="activity_battery_optimizations_summary">Disattiva le ottimizzazioni della batteria per la riproduzione multimediale quando lo schermo è spento.</string>
|
||||
<string name="activity_battery_optimizations_title">Ottimizzazioni della Batteria</string>
|
||||
<string name="activity_info_offline_mode">Modalità offline</string>
|
||||
<string name="album_bottom_sheet_add_to_playlist">Aggiungi alla playlist</string>
|
||||
<string name="album_bottom_sheet_add_to_queue">Aggiungi alla coda</string>
|
||||
<string name="album_bottom_sheet_download_all">Scarica tutto</string>
|
||||
<string name="album_bottom_sheet_go_to_artist">Vai all\'artista</string>
|
||||
<string name="album_bottom_sheet_instant_mix">Mix istantaneo</string>
|
||||
<string name="album_bottom_sheet_play_next">Riproduci successivo</string>
|
||||
<string name="album_bottom_sheet_remove_all">Rimuovi tutto</string>
|
||||
<string name="album_bottom_sheet_share">Condividi</string>
|
||||
<string name="album_bottom_sheet_shuffle">Riproduzione casuale</string>
|
||||
<string name="album_catalogue_title">Album</string>
|
||||
<string name="album_catalogue_title_expanded">Sfoglia Album</string>
|
||||
<string name="album_error_retrieving_artist">Errore nel recupero dell\'artista</string>
|
||||
<string name="album_list_page_downloaded">Album scaricati</string>
|
||||
<string name="album_list_page_most_played">Album più riprodotti</string>
|
||||
<string name="album_list_page_new_releases">Nuove uscite</string>
|
||||
<string name="album_list_page_recently_added">Album aggiunti di recente</string>
|
||||
<string name="album_list_page_recently_played">Album riprodotti di recente</string>
|
||||
<string name="album_list_page_starred">Album preferiti</string>
|
||||
<string name="album_list_page_title">Album</string>
|
||||
<string name="album_page_extra_info_button">Simili a questo</string>
|
||||
<string name="album_page_play_button">Riproduci</string>
|
||||
<string name="album_page_release_date_label">Rilasciato il %1$s</string>
|
||||
<string name="album_page_release_dates_label">Rilasciato il %1$s, originariamente il %2$s</string>
|
||||
<string name="album_page_shuffle_button">Riproduzione casuale</string>
|
||||
<string name="album_page_tracks_count_and_duration">%1$d brani • %2$d minuti</string>
|
||||
<string name="app_name">Tempus</string>
|
||||
<string name="artist_adapter_radio_station_starting">Ricerca in corso…</string>
|
||||
<string name="artist_bottom_sheet_instant_mix">Mix istantaneo</string>
|
||||
<string name="artist_bottom_sheet_shuffle">Riproduzione casuale</string>
|
||||
<string name="artist_catalogue_title">Artisti</string>
|
||||
<string name="artist_catalogue_title_expanded">Sfoglia Artisti</string>
|
||||
<string name="artist_error_retrieving_radio">Errore nel recupero della radio dell\'artista</string>
|
||||
<string name="artist_error_retrieving_tracks">Errore nel recupero dei brani dell\'artista</string>
|
||||
<string name="artist_list_page_downloaded">Artisti scaricati</string>
|
||||
<string name="artist_list_page_starred">Artisti preferiti</string>
|
||||
<string name="artist_list_page_title">Artisti</string>
|
||||
<string name="artist_page_radio_button">Radio</string>
|
||||
<string name="artist_page_shuffle_button">Riproduzione casuale</string>
|
||||
<string name="artist_page_switch_layout_button">Cambia layout</string>
|
||||
<string name="artist_page_title_album_more_like_this_button">Simili a questo</string>
|
||||
<string name="artist_page_title_album_section">Album</string>
|
||||
<string name="artist_page_title_biography_more_button">Altro</string>
|
||||
<string name="artist_page_title_biography_section">Biografia</string>
|
||||
<string name="artist_page_title_most_streamed_song_section">Brani più ascoltati</string>
|
||||
<string name="artist_page_title_most_streamed_song_see_all_button">Vedi tutto</string>
|
||||
<string name="battery_optimization_negative_button">Ignora</string>
|
||||
<string name="battery_optimization_neutral_button">Non chiedere di nuovo</string>
|
||||
<string name="battery_optimization_positive_button">Disabilita</string>
|
||||
<string name="connection_alert_dialog_negative_button">Annulla</string>
|
||||
<string name="connection_alert_dialog_neutral_button">Attiva risparmio dati</string>
|
||||
<string name="connection_alert_dialog_positive_button">OK</string>
|
||||
<string name="connection_alert_dialog_summary">L\'accesso al server Subsonic è stato limitato alle connessioni Wi-Fi. Per evitare che questo avviso riappaia, disabilita il controllo connessione nelle impostazioni dell\'app.</string>
|
||||
<string name="connection_alert_dialog_title">Wi-Fi non connesso</string>
|
||||
<string name="content_description_shuffle_button">Riproduzione casuale</string>
|
||||
<string name="delete_download_storage_dialog_negative_button">Annulla</string>
|
||||
<string name="delete_download_storage_dialog_positive_button">Continua</string>
|
||||
<string name="delete_download_storage_dialog_summary">Attenzione, procedendo questa azione eliminerà definitivamente tutti gli elementi scaricati da tutti i server.</string>
|
||||
<string name="delete_download_storage_dialog_title">Elimina elementi salvati</string>
|
||||
<string name="description_empty_title">Descrizione non disponibile</string>
|
||||
<string name="disc_titlefull">Disco %1$s - %2$s</string>
|
||||
<string name="disc_titleless">Disco %1$s</string>
|
||||
<string name="download_directory_dialog_negative_button">Annulla</string>
|
||||
<string name="download_directory_dialog_positive_button">Scarica</string>
|
||||
<string name="download_directory_dialog_summary">Tutti i brani in questa cartella verranno scaricati. I brani nelle sottocartelle non verranno scaricati.</string>
|
||||
<string name="download_directory_dialog_title">Scarica i brani</string>
|
||||
<string name="download_info_empty_subtitle">Una volta scaricato un brano, lo troverai qui</string>
|
||||
<string name="download_info_empty_title">Nessun download ancora!</string>
|
||||
<string name="download_item_multiple_subtitle_formatter">%1$s • %2$s elementi</string>
|
||||
<string name="download_item_single_subtitle_formatter">%1$s elementi</string>
|
||||
<string name="download_shuffle_all_subtitle">Riproduzione casuale di tutto</string>
|
||||
<string name="download_storage_dialog_sub_summary">Per rendere effettive le modifiche, riavvia l\'app.</string>
|
||||
<string name="download_storage_dialog_summary">Cambiare la destinazione dei file scaricati da una memoria all\'altra eliminerà immediatamente tutti i file scaricati precedentemente nella vecchia memoria.</string>
|
||||
<string name="download_storage_dialog_title">Seleziona opzione di memoria</string>
|
||||
<string name="download_storage_external_dialog_positive_button">Esterna</string>
|
||||
<string name="download_storage_internal_dialog_negative_button">Interna</string>
|
||||
<string name="download_title_section">Download</string>
|
||||
<string name="downloaded_bottom_sheet_add_to_queue">Aggiungi alla coda</string>
|
||||
<string name="downloaded_bottom_sheet_play_next">Riproduci successivo</string>
|
||||
<string name="downloaded_bottom_sheet_remove">Rimuovi</string>
|
||||
<string name="downloaded_bottom_sheet_remove_all">Rimuovi tutto</string>
|
||||
<string name="downloaded_bottom_sheet_shuffle">Riproduzione casuale</string>
|
||||
<string name="empty_string" />
|
||||
<string name="error_required">Obbligatorio</string>
|
||||
<string name="error_server_prefix">Prefisso http o https richiesto</string>
|
||||
<string name="exo_download_notification_channel_name">Download</string>
|
||||
<string name="filter_info_selection">Seleziona due o più filtri</string>
|
||||
<string name="filter_title">Filtro</string>
|
||||
<string name="filter_title_expanded">Filtra Generi</string>
|
||||
<string name="genre_catalogue_title">Catalogo dei Generi</string>
|
||||
<string name="genre_catalogue_title_expanded">Sfoglia Generi</string>
|
||||
<string name="github_update_dialog_negative_button">Ricordamelo più tardi</string>
|
||||
<string name="github_update_dialog_neutral_button">Supportami</string>
|
||||
<string name="github_update_dialog_positive_button">Scarica ora</string>
|
||||
<string name="github_update_dialog_summary">È disponibile una nuova versione dell\'app su Github.</string>
|
||||
<string name="github_update_dialog_title">Aggiornamento disponibile</string>
|
||||
<string name="home_rearrangement_dialog_negative_button">Annulla</string>
|
||||
<string name="home_rearrangement_dialog_neutral_button">Reimposta</string>
|
||||
<string name="home_rearrangement_dialog_positive_button">Salva</string>
|
||||
<string name="home_rearrangement_dialog_title">Riorganizza home</string>
|
||||
<string name="home_rearrangement_dialog_subtitle">Si prega di notare che per rendere effettive le modifiche è necessario riavviare l\'applicazione.</string>
|
||||
<string name="home_subtitle_best_of">Le migliori canzoni dei tuoi artisti preferiti</string>
|
||||
<string name="home_subtitle_made_for_you">Inizia un mix da una canzone che ti è piaciuta</string>
|
||||
<string name="home_subtitle_new_internet_radio_station">Aggiungi una nuova radio</string>
|
||||
<string name="home_subtitle_new_podcast_channel">Aggiungi un nuovo canale podcast</string>
|
||||
<string name="home_sync_starred_cancel">Annulla</string>
|
||||
<string name="home_sync_starred_download">Scarica</string>
|
||||
<string name="home_sync_starred_subtitle">Scaricare questi brani potrebbe comportare un uso significativo di dati</string>
|
||||
<string name="home_sync_starred_title">Sembra che ci siano brani da sincronizzare con una stella</string>
|
||||
<string name="home_title_best_of">Il meglio di</string>
|
||||
<string name="home_title_discovery">Scoperta</string>
|
||||
<string name="home_title_discovery_shuffle_all_button">Mescola tutto</string>
|
||||
<string name="home_title_flashback">Flashback</string>
|
||||
<string name="home_title_internet_radio_station">Stazioni radio internet</string>
|
||||
<string name="home_title_last_played">Ultimi ascolti</string>
|
||||
<string name="home_title_last_played_see_all_button">Vedi tutto</string>
|
||||
<string name="home_title_last_week">La scorsa settimana</string>
|
||||
<string name="home_title_last_month">Il mese scorso</string>
|
||||
<string name="home_title_last_year">L\'anno scorso</string>
|
||||
<string name="home_title_made_for_you">Fatto per te</string>
|
||||
<string name="home_title_most_played">Più ascoltati</string>
|
||||
<string name="home_title_most_played_see_all_button">Vedi tutto</string>
|
||||
<string name="home_title_new_releases">Nuove uscite</string>
|
||||
<string name="home_title_newest_podcasts">Podcast più recenti</string>
|
||||
<string name="home_title_pinned_playlists">Playlist</string>
|
||||
<string name="home_title_podcast_channels">Canali</string>
|
||||
<string name="home_title_podcast_channels_see_all_button">Vedi tutto</string>
|
||||
<string name="home_title_radio_station">Stazioni radio</string>
|
||||
<string name="home_title_recently_added">Aggiunti di recente</string>
|
||||
<string name="home_title_recently_added_see_all_button">Vedi tutto</string>
|
||||
<string name="home_title_shares">Condivisioni</string>
|
||||
<string name="home_title_starred_albums">★ Album con stella</string>
|
||||
<string name="home_title_starred_albums_see_all_button">Vedi tutto</string>
|
||||
<string name="home_title_starred_artists">★ Artisti con stella</string>
|
||||
<string name="home_title_starred_artists_see_all_button">Vedi tutto</string>
|
||||
<string name="home_title_starred_tracks">★ Brani con stella</string>
|
||||
<string name="home_title_starred_tracks_see_all_button">Vedi tutto</string>
|
||||
<string name="home_title_top_songs">I tuoi migliori brani</string>
|
||||
<string name="home_option_reorganize">Riorganizza</string>
|
||||
<string name="label_dot_separator" translatable="false">•</string>
|
||||
<string name="label_placeholder" translatable="false">--</string>
|
||||
<string name="library_title_album">Album</string>
|
||||
<string name="library_title_album_see_all_button">Vedi tutto</string>
|
||||
<string name="library_title_artist">Artisti</string>
|
||||
<string name="library_title_artist_see_all_button">Vedi tutto</string>
|
||||
<string name="library_title_genre">Generi</string>
|
||||
<string name="library_title_genre_see_all_button">Vedi tutto</string>
|
||||
<string name="library_title_music_folder">Cartelle musicali</string>
|
||||
<string name="library_title_playlist">Playlist</string>
|
||||
<string name="library_title_playlist_see_all_button">Vedi tutto</string>
|
||||
<string name="login_empty">Nessun server aggiunto</string>
|
||||
<string name="login_title">Server Subsonic</string>
|
||||
<string name="login_title_expanded">Server Subsonic</string>
|
||||
<string name="media_route_menu_title">Trasmetti</string>
|
||||
<string name="menu_add_button">Aggiungi</string>
|
||||
<string name="album_bottom_sheet_download_all">Scarica tutto</string>
|
||||
<string name="album_bottom_sheet_go_to_artist">Vai all\'artista</string>
|
||||
<string name="album_bottom_sheet_instant_mix">Mix istantaneo</string>
|
||||
<string name="album_bottom_sheet_play_next">Riproduci dopo</string>
|
||||
<string name="album_bottom_sheet_remove_all">Rimuovi tutto</string>
|
||||
<string name="album_bottom_sheet_share">Condividi</string>
|
||||
<string name="album_bottom_sheet_shuffle">Riproduzione casuale</string>
|
||||
<string name="album_catalogue_title">Album</string>
|
||||
<string name="album_catalogue_title_expanded">Sfoglia Album</string>
|
||||
<string name="album_error_retrieving_artist">Errore nel recupero dell\'artista</string>
|
||||
<string name="album_list_page_downloaded">Album scaricati</string>
|
||||
<string name="album_list_page_most_played">Album più riprodotti</string>
|
||||
<string name="album_list_page_new_releases">Nuove uscite</string>
|
||||
<string name="album_list_page_recently_added">Album aggiunti di recente</string>
|
||||
<string name="album_list_page_recently_played">Album riprodotti di recente</string>
|
||||
<string name="album_list_page_starred">Album preferiti</string>
|
||||
<string name="album_list_page_title">Album</string>
|
||||
<string name="album_page_extra_info_button">Altri simili</string>
|
||||
<string name="album_page_play_button">Riproduci</string>
|
||||
<string name="album_page_release_date_label">Rilasciato il %1$s</string>
|
||||
<string name="album_page_release_dates_label">Rilasciato il %1$s, originariamente il %2$s</string>
|
||||
<string name="album_page_shuffle_button">Riproduzione casuale</string>
|
||||
<string name="album_page_tracks_count_and_duration">%1$d brani • %2$d minuti</string>
|
||||
<string name="app_name">Tempus</string>
|
||||
<string name="artist_adapter_radio_station_starting">Cercando…</string>
|
||||
<string name="artist_bottom_sheet_instant_mix">Mix istantaneo</string>
|
||||
<string name="artist_bottom_sheet_shuffle">Riproduzione casuale</string>
|
||||
<string name="artist_catalogue_title">Artisti</string>
|
||||
<string name="artist_catalogue_title_expanded">Sfoglia Artisti</string>
|
||||
<string name="artist_error_retrieving_radio">Errore nel recupero della radio dell\'artista</string>
|
||||
<string name="artist_error_retrieving_tracks">Errore nel recupero dei brani dell\'artista</string>
|
||||
<string name="artist_list_page_downloaded">Artisti scaricati</string>
|
||||
<string name="artist_list_page_starred">Artisti preferiti</string>
|
||||
<string name="artist_list_page_title">Artisti</string>
|
||||
<string name="artist_page_radio_button">Radio</string>
|
||||
<string name="artist_page_shuffle_button">Riproduzione casuale</string>
|
||||
<string name="artist_page_switch_layout_button">Cambia layout</string>
|
||||
<string name="artist_page_title_album_more_like_this_button">Altri simili</string>
|
||||
<string name="artist_page_title_album_section">Album</string>
|
||||
<string name="artist_page_title_biography_more_button">Altro</string>
|
||||
<string name="artist_page_title_biography_section">Biografia</string>
|
||||
<string name="artist_page_title_most_streamed_song_section">Brani più ascoltati</string>
|
||||
<string name="artist_page_title_most_streamed_song_see_all_button">Vedi tutto</string>
|
||||
<string name="battery_optimization_negative_button">Ignora</string>
|
||||
<string name="battery_optimization_neutral_button">Non chiedere di nuovo</string>
|
||||
<string name="battery_optimization_positive_button">Disabilita</string>
|
||||
<string name="connection_alert_dialog_negative_button">Annulla</string>
|
||||
<string name="connection_alert_dialog_neutral_button">Attiva risparmio dati</string>
|
||||
<string name="connection_alert_dialog_positive_button">OK</string>
|
||||
<string name="connection_alert_dialog_summary">L\'accesso al server Subsonic è stato limitato alle connessioni Wi-Fi. Per evitare che questo avviso riappaia, disabilita il controllo connessione nelle impostazioni dell\'app.</string>
|
||||
<string name="connection_alert_dialog_title">Wi-Fi non connesso</string>
|
||||
<string name="content_description_shuffle_button">Riproduzione casuale</string>
|
||||
<string name="delete_download_storage_dialog_negative_button">Annulla</string>
|
||||
<string name="delete_download_storage_dialog_positive_button">Continua</string>
|
||||
<string name="delete_download_storage_dialog_summary">Attenzione, procedendo questa azione eliminerà definitivamente tutti gli elementi scaricati da tutti i server.</string>
|
||||
<string name="delete_download_storage_dialog_title">Elimina elementi salvati</string>
|
||||
<string name="description_empty_title">Descrizione non disponibile</string>
|
||||
<string name="disc_titlefull">Disco %1$s - %2$s</string>
|
||||
<string name="disc_titleless">Disco %1$s</string>
|
||||
<string name="download_directory_dialog_negative_button">Annulla</string>
|
||||
<string name="download_directory_dialog_positive_button">Scarica</string>
|
||||
<string name="download_directory_dialog_summary">Tutti i brani in questa cartella verranno scaricati. I brani nelle sottocartelle non verranno scaricati.</string>
|
||||
<string name="download_directory_dialog_title">Scarica i brani</string>
|
||||
<string name="download_directory_set">Imposta dove scaricare la musica</string>
|
||||
<string name="download_info_empty_subtitle">Una volta scaricato un brano, lo troverai qui</string>
|
||||
<string name="download_info_empty_title">Ancora nessun download!</string>
|
||||
<string name="download_item_multiple_subtitle_formatter">%1$s • %2$s elementi</string>
|
||||
<string name="download_item_single_subtitle_formatter">%1$s elementi</string>
|
||||
<string name="download_shuffle_all_subtitle">Riproduzione casuale di tutto</string>
|
||||
<string name="download_storage_dialog_sub_summary">Per rendere effettive le modifiche, riavvia l\'app.</string>
|
||||
<string name="download_storage_dialog_summary">Cambiare la destinazione dei file scaricati da una memoria all\'altra eliminerà immediatamente tutti i file scaricati precedentemente nella vecchia memoria.</string>
|
||||
<string name="download_storage_dialog_title">Seleziona opzione di memoria</string>
|
||||
<string name="download_storage_external_dialog_positive_button">Esterna</string>
|
||||
<string name="download_storage_internal_dialog_negative_button">Interna</string>
|
||||
<string name="download_storage_directory_dialog_neutral_button">Cartella</string>
|
||||
<string name="download_title_section">Scarica</string>
|
||||
<string name="download_refresh_no_directory">Imposta una cartella di download per aggiornare i tuoi download.</string>
|
||||
<string name="download_refresh_no_changes">Nessun download mancante trovato.</string>
|
||||
<plurals name="download_refresh_removed">
|
||||
<item quantity="one">Rimosso %d download mancante.</item>
|
||||
<item quantity="other">Rimossi %d download mancanti.</item>
|
||||
</plurals>
|
||||
<string name="download_refresh_button_content_description">Aggiorna gli elementi scaricati</string>
|
||||
<string name="downloaded_bottom_sheet_add_to_queue">Aggiungi alla coda</string>
|
||||
<string name="downloaded_bottom_sheet_play_next">Riproduci dopo</string>
|
||||
<string name="downloaded_bottom_sheet_remove">Rimuovi</string>
|
||||
<string name="downloaded_bottom_sheet_remove_all">Rimuovi tutto</string>
|
||||
<string name="downloaded_bottom_sheet_shuffle">Riproduzione casuale</string>
|
||||
<string name="empty_string" />
|
||||
<string name="error_required">Obbligatorio</string>
|
||||
<string name="error_server_prefix">Prefisso http o https richiesto</string>
|
||||
<string name="exo_download_notification_channel_name">Download</string>
|
||||
<string name="exo_controls_heart_off_description">Aggiungi ai preferiti</string>
|
||||
<string name="exo_controls_heart_on_description">Rimuovi dai preferiti</string>
|
||||
<string name="cast_expanded_controller_loading">Caricamento…</string>
|
||||
<string name="filter_info_selection">Seleziona due o più filtri</string>
|
||||
<string name="filter_title">Filtro</string>
|
||||
<string name="filter_artist">Filtra artisti</string>
|
||||
<string name="filter_title_expanded">Filtra Generi</string>
|
||||
<string name="generic_list_page_count">(%1$d)</string>
|
||||
<string name="generic_list_page_count_unknown">(+%1$d)</string>
|
||||
<string name="genre_catalogue_title">Catalogo dei Generi</string>
|
||||
<string name="genre_catalogue_title_expanded">Sfoglia Generi</string>
|
||||
<string name="github_update_dialog_negative_button">Ricordamelo più tardi</string>
|
||||
<string name="github_update_dialog_neutral_button">Supportami</string>
|
||||
<string name="github_update_dialog_positive_button">Scarica ora</string>
|
||||
<string name="github_update_dialog_summary">È disponibile una nuova versione dell\'app su Github.</string>
|
||||
<string name="github_update_dialog_title">Aggiornamento disponibile</string>
|
||||
<string name="home_rearrangement_dialog_negative_button">Annulla</string>
|
||||
<string name="home_rearrangement_dialog_neutral_button">Ripristina</string>
|
||||
<string name="home_rearrangement_dialog_positive_button">Salva</string>
|
||||
<string name="home_rearrangement_dialog_title">Riorganizza home</string>
|
||||
<string name="home_rearrangement_dialog_subtitle">Per rendere effettive le modifiche è necessario riavviare l\'applicazione.</string>
|
||||
<string name="home_section_music">Musica</string>
|
||||
<string name="home_section_podcast">Podcast</string>
|
||||
<string name="home_section_radio">Radio</string>
|
||||
<string name="home_subtitle_best_of">Le migliori canzoni dei tuoi artisti preferiti</string>
|
||||
<string name="home_subtitle_made_for_you">Inizia un mix da una canzone che ti è piaciuta</string>
|
||||
<string name="home_subtitle_new_internet_radio_station">Aggiungi una nuova radio</string>
|
||||
<string name="home_subtitle_new_podcast_channel">Aggiungi un nuovo canale podcast</string>
|
||||
<string name="home_sync_starred_cancel">Annulla</string>
|
||||
<string name="home_sync_starred_download">Scarica</string>
|
||||
<string name="home_sync_starred_subtitle">Scaricare questi brani potrebbe comportare un uso significativo di dati</string>
|
||||
<string name="home_sync_starred_title">Sembra che ci siano alcuni brani preferiti da sincronizzare</string>
|
||||
<string name="home_sync_starred_albums_title">Sincronizza Album Preferiti</string>
|
||||
<string name="home_sync_starred_albums_subtitle">Gli album preferiti saranno disponibili offline</string>
|
||||
<string name="home_sync_starred_artists_title">Sincronizza Artisti Preferiti</string>
|
||||
<string name="home_sync_starred_artists_subtitle">Hai artisti preferiti con musica non scaricata</string>
|
||||
<string name="home_title_best_of">Il meglio di</string>
|
||||
<string name="home_title_discovery">Scopri</string>
|
||||
<string name="home_title_discovery_shuffle_all_button">Mescola tutto</string>
|
||||
<string name="home_title_flashback">Flashback</string>
|
||||
<string name="home_title_internet_radio_station">Stazioni internet-radio</string>
|
||||
<string name="home_title_last_played">Ultimi ascolti</string>
|
||||
<string name="home_title_last_played_see_all_button">Vedi tutto</string>
|
||||
<string name="home_title_last_week">La scorsa settimana</string>
|
||||
<string name="home_title_last_month">Il mese scorso</string>
|
||||
<string name="home_title_last_year">L\'anno scorso</string>
|
||||
<string name="home_title_made_for_you">Fatto per te</string>
|
||||
<string name="home_title_most_played">Più ascoltati</string>
|
||||
<string name="home_title_most_played_see_all_button">Vedi tutto</string>
|
||||
<string name="home_title_new_releases">Nuove uscite</string>
|
||||
<string name="home_title_newest_podcasts">Podcast più recenti</string>
|
||||
<string name="home_title_pinned_playlists">Playlist</string>
|
||||
<string name="home_title_podcast_channels">Canali</string>
|
||||
<string name="home_title_podcast_channels_see_all_button">Vedi tutto</string>
|
||||
<string name="home_title_radio_station">Stazioni radio</string>
|
||||
<string name="home_title_recently_added">Aggiunti di recente</string>
|
||||
<string name="home_title_recently_added_see_all_button">Vedi tutto</string>
|
||||
<string name="home_title_shares">Condivisioni</string>
|
||||
<string name="home_title_starred_albums">★ Album preferiti</string>
|
||||
<string name="home_title_starred_albums_see_all_button">Vedi tutto</string>
|
||||
<string name="home_title_starred_artists">★ Artisti preferiti</string>
|
||||
<string name="home_title_starred_artists_see_all_button">Vedi tutto</string>
|
||||
<string name="home_title_starred_tracks">★ Brani preferiti</string>
|
||||
<string name="home_title_starred_tracks_see_all_button">Vedi tutto</string>
|
||||
<string name="home_title_top_songs">I tuoi migliori brani</string>
|
||||
<string name="home_option_reorganize">Riorganizza</string>
|
||||
<string name="label_dot_separator" translatable="false">•</string>
|
||||
<string name="label_placeholder" translatable="false">--</string>
|
||||
<string name="library_title_album">Album</string>
|
||||
<string name="library_title_album_see_all_button">Vedi tutto</string>
|
||||
<string name="library_title_artist">Artisti</string>
|
||||
<string name="library_title_artist_see_all_button">Vedi tutto</string>
|
||||
<string name="library_title_genre">Generi</string>
|
||||
<string name="library_title_genre_see_all_button">Vedi tutto</string>
|
||||
<string name="library_title_music_folder">Cartelle della musica</string>
|
||||
<string name="library_title_playlist">Playlist</string>
|
||||
<string name="library_title_playlist_see_all_button">Vedi tutto</string>
|
||||
<string name="login_empty">Nessun server aggiunto</string>
|
||||
<string name="login_title">Server Subsonic</string>
|
||||
<string name="login_title_expanded">Server Subsonic</string>
|
||||
<string name="media_route_menu_title">Trasmetti</string>
|
||||
<string name="menu_add_button">Aggiungi</string>
|
||||
<string name="menu_add_to_playlist_button">Aggiungi alla playlist</string>
|
||||
<string name="menu_download_all_button">Scarica tutto</string>
|
||||
<string name="menu_download_label">Scarica</string>
|
||||
<string name="menu_filter_all">Tutti</string>
|
||||
<string name="menu_filter_download">Scaricati</string>
|
||||
<string name="menu_group_by_album">Album</string>
|
||||
<string name="menu_group_by_artist">Artista</string>
|
||||
<string name="menu_group_by_genre">Genere</string>
|
||||
<string name="menu_group_by_track">Brano</string>
|
||||
<string name="menu_group_by_year">Anno</string>
|
||||
<string name="menu_home_label">Home</string>
|
||||
<string name="menu_last_week_name">La scorsa settimana</string>
|
||||
<string name="menu_last_month_name">Il mese scorso</string>
|
||||
<string name="menu_last_year_name">L\'anno scorso</string>
|
||||
<string name="menu_library_label">Libreria</string>
|
||||
<string name="menu_search_button">Cerca</string>
|
||||
<string name="menu_settings_button">Impostazioni</string>
|
||||
<string name="menu_sort_artist">Artista</string>
|
||||
<string name="menu_sort_name">Nome</string>
|
||||
<string name="menu_sort_random">Casuale</string>
|
||||
<string name="menu_sort_recently_added">Aggiunti di recente</string>
|
||||
<string name="menu_pin_button">Aggiungi alla schermata home</string>
|
||||
<string name="menu_unpin_button">Rimuovi dalla schermata home</string>
|
||||
<string name="menu_sort_year">Anno</string>
|
||||
<string name="player_playback_speed">%1$.2fx</string>
|
||||
<string name="player_queue_clean_all_button">Svuota coda di riproduzione</string>
|
||||
<string name="menu_rate_album">Valuta l\'album</string>
|
||||
<string name="menu_download_label">Scarica</string>
|
||||
<string name="menu_filter_all">Tutti</string>
|
||||
<string name="menu_filter_download">Scaricati</string>
|
||||
<string name="menu_group_by_album">Album</string>
|
||||
<string name="menu_group_by_artist">Artista</string>
|
||||
<string name="menu_group_by_genre">Genere</string>
|
||||
<string name="menu_group_by_track">Brano</string>
|
||||
<string name="menu_group_by_year">Anno</string>
|
||||
<string name="menu_home_label">Home</string>
|
||||
<string name="menu_last_week_name">La scorsa settimana</string>
|
||||
<string name="menu_last_month_name">Il mese scorso</string>
|
||||
<string name="menu_last_year_name">L\'anno scorso</string>
|
||||
<string name="menu_library_label">Libreria</string>
|
||||
<string name="menu_search_button">Cerca</string>
|
||||
<string name="menu_settings_button">Impostazioni</string>
|
||||
<string name="menu_sort_artist">Artista</string>
|
||||
<string name="menu_sort_name">Nome</string>
|
||||
<string name="menu_sort_random">Casuale</string>
|
||||
<string name="menu_sort_album_count">Numero di Album</string>
|
||||
<string name="menu_sort_recently_added">Aggiunti di recente</string>
|
||||
<string name="menu_sort_recently_played">Riprodotti di recente</string>
|
||||
<string name="menu_sort_most_played">Più riprodotti</string>
|
||||
<string name="menu_sort_most_recently_starred">Preferiti più recentemente</string>
|
||||
<string name="menu_sort_least_recently_starred">Preferiti meno recentemente</string>
|
||||
<string name="menu_pin_button">Aggiungi alla schermata home</string>
|
||||
<string name="menu_unpin_button">Rimuovi dalla schermata home</string>
|
||||
<string name="menu_sort_year">Anno</string>
|
||||
<string name="player_playback_speed">%1$.2fx</string>
|
||||
<string name="player_queue_clean_all_button">Svuota coda di riproduzione</string>
|
||||
<string name="player_queue_save_queue_success">Salvato</string>
|
||||
<string name="player_server_priority">Priorità server</string>
|
||||
<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_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>
|
||||
<string name="playlist_counted_tracks">%1$d brani • %2$s</string>
|
||||
<string name="playlist_duration">Durata • %1$s</string>
|
||||
<string name="playlist_editor_dialog_action_delete_toast">Premi a lungo per eliminare</string>
|
||||
<string name="playlist_editor_dialog_hint_name">Nome della playlist</string>
|
||||
<string name="playlist_editor_dialog_negative_button">Annulla</string>
|
||||
<string name="playlist_editor_dialog_neutral_button">Elimina</string>
|
||||
<string name="playlist_editor_dialog_positive_button">Salva</string>
|
||||
<string name="playlist_editor_dialog_title">Modifica playlist</string>
|
||||
<string name="playlist_page_play_button">Riproduci</string>
|
||||
<string name="playlist_page_shuffle_button">Mescola</string>
|
||||
<string name="playlist_song_count">Playlist • %1$d brani</string>
|
||||
<string name="podcast_bottom_sheet_add_to_queue">Aggiungi alla coda</string>
|
||||
<string name="podcast_bottom_sheet_delete">Elimina</string>
|
||||
<string name="podcast_bottom_sheet_download">Scarica</string>
|
||||
<string name="podcast_bottom_sheet_go_to_channel">Vai al canale</string>
|
||||
<string name="podcast_bottom_sheet_play_next">Riproduci dopo</string>
|
||||
<string name="podcast_bottom_sheet_remove">Rimuovi</string>
|
||||
<string name="podcast_channel_catalogue_title">Canali</string>
|
||||
<string name="podcast_channel_catalogue_title_expanded">Sfoglia Canali</string>
|
||||
<string name="podcast_channel_editor_dialog_hint_rss_url">URL RSS</string>
|
||||
<string name="podcast_channel_editor_dialog_title">Canale Podcast</string>
|
||||
<string name="podcast_channel_page_title_description_section">Descrizione</string>
|
||||
<string name="podcast_channel_page_title_episode_section">Episodi</string>
|
||||
<string name="podcast_channel_page_title_no_episode_available">Nessun episodio disponibile</string>
|
||||
<string name="podcast_episode_download_request_snackbar">La tua richiesta è stata inviata al server</string>
|
||||
<string name="podcast_info_empty_button">Clicca per nascondere la sezione\nGli effetti saranno visibili al riavvio</string>
|
||||
<string name="podcast_info_empty_subtitle">Una volta aggiunto un canale, lo troverai qui</string>
|
||||
<string name="podcast_info_empty_title">Nessun podcast trovato!</string>
|
||||
<string name="podcast_release_date_duration_formatter">%1$s • %2$s</string>
|
||||
<string name="radio_editor_dialog_hint_homepage_url">URL Homepage Radio</string>
|
||||
<string name="radio_editor_dialog_hint_name">Nome Radio</string>
|
||||
<string name="radio_editor_dialog_hint_stream_url">URL Stream Radio</string>
|
||||
<string name="radio_editor_dialog_negative_button">Annulla</string>
|
||||
<string name="radio_editor_dialog_neutral_button">Elimina</string>
|
||||
<string name="radio_editor_dialog_positive_button">Salva</string>
|
||||
<string name="radio_editor_dialog_title">Stazione Radio Internet</string>
|
||||
<string name="radio_station_info_empty_button">Clicca per nascondere la sezione\nGli effetti saranno visibili al riavvio</string>
|
||||
<string name="radio_station_info_empty_subtitle">Una volta aggiunta una stazione radio, la troverai qui</string>
|
||||
<string name="radio_station_info_empty_title">Nessuna stazione trovata!</string>
|
||||
<string name="rating_dialog_negative_button">Annulla</string>
|
||||
<string name="rating_dialog_positive_button">Salva</string>
|
||||
<string name="rating_dialog_title">Valuta</string>
|
||||
<string name="search_hint">Cerca titolo, artisti o album</string>
|
||||
<string name="search_info_minimum_characters">Inserisci almeno tre caratteri</string>
|
||||
<string name="search_title_album">Album</string>
|
||||
<string name="search_title_artist">Artisti</string>
|
||||
<string name="search_title_song">Brani</string>
|
||||
<string name="server_signup_dialog_action_low_security">Bassa sicurezza</string>
|
||||
<string name="server_signup_dialog_action_delete_toast">Premi a lungo per eliminare</string>
|
||||
<string name="server_signup_dialog_hint_local_address">URL locale</string>
|
||||
<string name="server_signup_dialog_hint_name">Nome Server</string>
|
||||
<string name="server_signup_dialog_hint_password">Password</string>
|
||||
<string name="server_signup_dialog_hint_url">URL Server</string>
|
||||
<string name="server_signup_dialog_hint_username">Nome utente</string>
|
||||
<string name="server_signup_dialog_negative_button">Annulla</string>
|
||||
<string name="server_signup_dialog_neutral_button">Elimina</string>
|
||||
<string name="server_signup_dialog_positive_button">Salva</string>
|
||||
<string name="server_signup_dialog_title">Aggiungi server</string>
|
||||
<string name="server_unreachable_dialog_negative_button">Annulla</string>
|
||||
<string name="server_unreachable_dialog_neutral_button">Vai al login</string>
|
||||
<string name="server_unreachable_dialog_positive_button">Continua comunque</string>
|
||||
<string name="server_unreachable_dialog_summary">Il server richiesto non è disponibile. Se scegli di continuare, questo messaggio non apparirà per la prossima ora.</string>
|
||||
<string name="server_unreachable_dialog_title">Server irraggiungibile</string>
|
||||
<string name="settings_about_summary">Tempus è un client musicale open source e leggero per Subsonic, progettato e costruito nativamente per Android.</string>
|
||||
<string name="settings_about_title">Informazioni</string>
|
||||
<string name="settings_always_on_display">Sempre attivo</string>
|
||||
<string name="settings_audio_transcode_download_format">Formato transcodifica</string>
|
||||
<string name="settings_audio_transcode_download_priority_summary">Se abilitato, Tempus non forzerà il download del brano con le impostazioni di transcodifica sottostanti.</string>
|
||||
<string name="settings_audio_transcode_download_priority_title">Dare priorità alle impostazioni del server per lo streaming nei download</string>
|
||||
<string name="settings_audio_transcode_download_summary">Se abilitato, Tempus scaricherà i brani transcodificati.</string>
|
||||
<string name="settings_audio_transcode_download_title">Scarica brani transcodificati</string>
|
||||
<string name="settings_audio_transcode_estimate_content_length_summary">Se abilitato, verrà richiesto al server di fornire la durata stimata del brano.</string>
|
||||
<string name="settings_audio_transcode_estimate_content_length_title">Stima della lunghezza del contenuto</string>
|
||||
<string name="settings_audio_transcode_format_download">Formato transcodifica per download</string>
|
||||
<string name="settings_audio_transcode_format_mobile">Formato transcodifica su mobile</string>
|
||||
<string name="settings_audio_transcode_format_wifi">Formato transcodifica su Wi-Fi</string>
|
||||
<string name="settings_audio_transcode_priority_summary">Se abilitato, Tempus non forzerà lo streaming del brano con le impostazioni di transcodifica sottostanti.</string>
|
||||
<string name="settings_audio_transcode_priority_title">Dare priorità alle impostazioni di transcodifica del server</string>
|
||||
<string name="settings_audio_transcode_priority_toast">Priorità di transcodifica del brano assegnata al server</string>
|
||||
<string name="settings_buffering_strategy">Strategia di buffering</string>
|
||||
<string name="settings_buffering_strategy_summary">Perché la modifica abbia effetto è necessario riavviare manualmente l\'app.</string>
|
||||
<string name="settings_continuous_play_summary">Consente alla musica di continuare a suonare dopo la fine di una playlist, riproducendo brani simili</string>
|
||||
<string name="settings_continuous_play_title">Riproduzione continua</string>
|
||||
<string name="settings_covers_cache">Dimensione della cache delle copertine</string>
|
||||
<string name="settings_data_saving_mode_summary">Per ridurre il consumo di dati, evita di scaricare le copertine.</string>
|
||||
<string name="settings_data_saving_mode_title">Limita utilizzo dei dati mobili</string>
|
||||
<string name="settings_delete_download_storage_summary">Continuando, tutti gli elementi salvati verranno eliminati in modo irreversibile.</string>
|
||||
<string name="settings_delete_download_storage_title">Elimina elementi salvati</string>
|
||||
<string name="settings_download_storage_title">Archivio download</string>
|
||||
<string name="settings_system_equalizer_summary">Regola le impostazioni audio</string>
|
||||
<string name="settings_system_equalizer_title">Equalizzatore di sistema</string>
|
||||
<string name="settings_github_link">https://github.com/eddyizm/tempus</string>
|
||||
<string name="settings_github_summary">Segui lo sviluppo</string>
|
||||
<string name="settings_github_title">Github</string>
|
||||
<string name="settings_image_size">Imposta risoluzione delle immagini</string>
|
||||
<string name="settings_language">Lingua</string>
|
||||
<string name="settings_logout_title">Esci</string>
|
||||
<string name="settings_max_bitrate_download">Bitrate per download</string>
|
||||
<string name="settings_max_bitrate_mobile">Bitrate su mobile</string>
|
||||
<string name="settings_max_bitrate_wifi">Bitrate su Wi-Fi</string>
|
||||
<string name="settings_media_cache">Dimensione della cache dei file multimediali</string>
|
||||
<string name="settings_music_directory">Mostra directory musicali</string>
|
||||
<string name="settings_music_directory_summary">Se abilitato, mostra la sezione delle directory musicali. Nota che per la navigazione nelle cartelle è necessario che il server supporti questa funzionalità.</string>
|
||||
<string name="settings_podcast">Mostra podcast</string>
|
||||
<string name="settings_podcast_summary">Se abilitato, mostra la sezione podcast. Riavvia l\'app per rendere effettive le modifiche.</string>
|
||||
<string name="settings_audio_quality">Mostra qualità audio</string>
|
||||
<string name="settings_audio_quality_summary">Il bitrate e il formato audio saranno mostrati per ogni traccia.</string>
|
||||
<string name="settings_item_rating">Mostra valutazione</string>
|
||||
<string name="settings_item_rating_summary">Se abilitato, verrà mostrata la valutazione dell\'elemento e se è contrassegnato come preferito.</string>
|
||||
<string name="settings_queue_syncing_countdown">Timer sincronizzazione</string>
|
||||
<string name="settings_queue_syncing_summary">Se abilitato, l\'utente avrà la possibilità di salvare la propria coda di riproduzione e potrà caricare lo stato all\'apertura dell\'applicazione.</string>
|
||||
<string name="settings_queue_syncing_title">Sincronizza coda di riproduzione per questo utente</string>
|
||||
<string name="settings_radio">Mostra radio</string>
|
||||
<string name="settings_radio_summary">Se abilitato, mostra la sezione radio. Riavvia l\'app per applicare completamente le modifiche.</string>
|
||||
<string name="settings_replay_gain">Imposta modalità di guadagno di riproduzione</string>
|
||||
<string name="settings_rounded_corner">Angoli arrotondati</string>
|
||||
<string name="settings_rounded_corner_size">Dimensione angoli</string>
|
||||
<string name="settings_rounded_corner_size_summary">Imposta la magnitudine dell\'angolo di curvatura.</string>
|
||||
<string name="settings_rounded_corner_summary">Se abilitato, imposta un angolo di curvatura per tutte le copertine visualizzate. Le modifiche avranno effetto al riavvio.</string>
|
||||
<string name="settings_scan_title">Scansiona libreria</string>
|
||||
<string name="settings_scrobble_title">Abilita scrobbling musicale</string>
|
||||
<string name="settings_share_title">Abilita condivisione musicale</string>
|
||||
<string name="settings_streaming_cache_size">Dimensione cache streaming</string>
|
||||
<string name="settings_streaming_cache_storage_title">Archiviazione cache streaming</string>
|
||||
<string name="settings_sub_summary_scrobble">È importante notare che lo scrobbling si basa anche sul fatto che il server sia abilitato a ricevere questi dati.</string>
|
||||
<string name="settings_summary_skip_min_star_rating">Quando si ascolta la radio di un artista, un mix istantaneo o quando si mescolano tutti i brani, i brani sotto una certa valutazione dell\'utente verranno ignorati.</string>
|
||||
<string name="settings_summary_replay_gain">Il guadagno di riproduzione è una funzionalità che consente di regolare il livello del volume delle tracce audio per un\'esperienza di ascolto coerente. Questa impostazione è efficace solo se la traccia contiene i metadati necessari.</string>
|
||||
<string name="settings_summary_scrobble">Lo scrobbling è una funzionalità che consente al tuo dispositivo di inviare informazioni sulle canzoni che ascolti al server musicale. Queste informazioni aiutano a creare raccomandazioni personalizzate in base alle tue preferenze musicali.</string>
|
||||
<string name="settings_summary_share">Permette all\'utente di condividere musica tramite un link. La funzionalità deve essere supportata e abilitata sul server ed è limitata a brani, album e playlist singoli.</string>
|
||||
<string name="settings_summary_syncing">Restituisce lo stato della coda di riproduzione per questo utente. Ciò include i brani nella coda di riproduzione, il brano attualmente in riproduzione e la posizione all\'interno di questo brano. Il server deve supportare questa funzionalità.</string>
|
||||
<string name="settings_summary_streaming_cache_size">%1$s \nAttualmente in uso: %2$s MiB</string>
|
||||
<string name="settings_summary_transcoding">Priorità data alla modalità di transcoding. Se impostato su "Riproduzione diretta", il bitrate del file non verrà modificato.</string>
|
||||
<string name="settings_summary_transcoding_download">Scarica media transcodificati. Se abilitato, l\'endpoint di download non verrà utilizzato, ma le impostazioni seguenti. \n\n Se "Formato di transcodifica per i download" è impostato su "Download diretto", il bitrate del file non verrà modificato.</string>
|
||||
<string name="settings_summary_transcoding_estimate_content_length">Quando il file viene transcodificato al volo, il client di solito non mostra la lunghezza della traccia. È possibile richiedere ai server che supportano la funzionalità di stimare la durata della traccia in riproduzione, ma i tempi di risposta possono essere più lunghi.</string>
|
||||
<string name="settings_sync_starred_tracks_for_offline_use_summary">Se abilitato, le tracce contrassegnate verranno scaricate per l\'uso offline.</string>
|
||||
<string name="settings_sync_starred_tracks_for_offline_use_title">Sincronizza tracce contrassegnate per uso offline</string>
|
||||
<string name="settings_theme">Tema</string>
|
||||
<string name="settings_title_data">Dati</string>
|
||||
<string name="settings_title_general">Generale</string>
|
||||
<string name="settings_title_rating">Valutazione</string>
|
||||
<string name="settings_title_replay_gain">Guadagno di riproduzione</string>
|
||||
<string name="settings_title_scrobble">Scrobble</string>
|
||||
<string name="settings_title_skip_min_star_rating">Ignora brani in base alla valutazione</string>
|
||||
<string name="settings_title_skip_min_star_rating_dialog">Brani con una valutazione di:</string>
|
||||
<string name="settings_title_share">Condividi</string>
|
||||
<string name="settings_title_syncing">Sincronizzazione</string>
|
||||
<string name="settings_title_transcoding">Transcoding</string>
|
||||
<string name="settings_title_transcoding_download">Download di Transcoding</string>
|
||||
<string name="settings_title_ui">Interfaccia utente</string>
|
||||
<string name="settings_transcoded_download">Download transcodificato</string>
|
||||
<string name="settings_version_summary" translatable="false">3.1.0</string>
|
||||
<string name="settings_version_title">Versione</string>
|
||||
<string name="settings_wifi_only_summary">Chiedi conferma all\'utente prima di effettuare streaming su rete mobile.</string>
|
||||
<string name="settings_wifi_only_title">Streaming solo tramite Wi-Fi avviso</string>
|
||||
<string name="share_bottom_sheet_copy_link">Copia link</string>
|
||||
<string name="share_bottom_sheet_delete">Elimina condivisione</string>
|
||||
<string name="share_bottom_sheet_update">Aggiorna condivisione</string>
|
||||
<string name="share_subtitle_item">Data di scadenza: %1$s</string>
|
||||
<string name="share_unsupported_error">La condivisione non è supportata o non è abilitata</string>
|
||||
<string name="share_update_dialog_hint_description">Descrizione</string>
|
||||
<string name="share_update_dialog_hint_expiration_date">Data di scadenza</string>
|
||||
<string name="share_update_dialog_negative_button">Annulla</string>
|
||||
<string name="share_update_dialog_positive_button">Salva</string>
|
||||
<string name="share_update_dialog_title">Condividi</string>
|
||||
<string name="song_bottom_sheet_add_to_playlist">Aggiungi alla playlist</string>
|
||||
<string name="song_bottom_sheet_add_to_queue">Aggiungi alla coda</string>
|
||||
<string name="song_bottom_sheet_download">Scarica</string>
|
||||
<string name="song_bottom_sheet_error_retrieving_album">Errore nel recupero dell\'album</string>
|
||||
<string name="song_bottom_sheet_error_retrieving_artist">Errore nel recupero dell\'artista</string>
|
||||
<string name="song_bottom_sheet_go_to_album">Vai all\'album</string>
|
||||
<string name="song_bottom_sheet_go_to_artist">Vai all\'artista</string>
|
||||
<string name="song_bottom_sheet_instant_mix">Mix istantaneo</string>
|
||||
<string name="song_bottom_sheet_play_next">Riproduci dopo</string>
|
||||
<string name="song_bottom_sheet_rate">Valuta</string>
|
||||
<string name="song_bottom_sheet_remove">Rimuovi</string>
|
||||
<string name="song_bottom_sheet_share">Condividi</string>
|
||||
<string name="song_list_page_downloaded">Scaricato</string>
|
||||
<string name="song_list_page_most_played">Tracce più riprodotte</string>
|
||||
<string name="song_list_page_recently_added">Tracce aggiunte di recente</string>
|
||||
<string name="song_list_page_recently_played">Tracce riprodotte di recente</string>
|
||||
<string name="song_list_page_starred">Tracce contrassegnate</string>
|
||||
<string name="song_list_page_top">Le migliori tracce di %1$s</string>
|
||||
<string name="song_list_page_year">Anno %1$d</string>
|
||||
<string name="song_subtitle_formatter">%1$s • %2$s %3$s</string>
|
||||
<string name="starred_sync_dialog_negative_button">Annulla</string>
|
||||
<string name="starred_sync_dialog_neutral_button">Continua</string>
|
||||
<string name="starred_sync_dialog_positive_button">Continua e scarica</string>
|
||||
<string name="starred_sync_dialog_summary">Il download delle tracce contrassegnate potrebbe richiedere una grande quantità di dati.</string>
|
||||
<string name="starred_sync_dialog_title">Sincronizza tracce contrassegnate</string>
|
||||
<string name="streaming_cache_storage_dialog_sub_summary">Per rendere effettive le modifiche, riavvia l\'app.</string>
|
||||
<string name="streaming_cache_storage_dialog_summary">Cambiare la destinazione dei file memorizzati nella cache da un\'unità di archiviazione a un\'altra può comportare la cancellazione di eventuali file memorizzati nella cache in precedenza nell\'altra unità di archiviazione.</string>
|
||||
<string name="streaming_cache_storage_dialog_title">Seleziona opzione di archiviazione</string>
|
||||
<string name="streaming_cache_storage_external_dialog_positive_button">Esterno</string>
|
||||
<string name="streaming_cache_storage_internal_dialog_negative_button">Interno</string>
|
||||
<string name="support_url">https://buymeacoffee.com/a.cappiello</string>
|
||||
<string name="track_info_album">Album</string>
|
||||
<string name="track_info_artist">Artista</string>
|
||||
<string name="track_info_bitrate">Bitrate</string>
|
||||
<string name="track_info_content_type">Tipo di contenuto</string>
|
||||
<string name="track_info_dialog_positive_button">OK</string>
|
||||
<string name="track_info_dialog_title">Info traccia</string>
|
||||
<string name="track_info_disc_number">Numero del disco</string>
|
||||
<string name="track_info_duration">Durata</string>
|
||||
<string name="track_info_genre">Genere</string>
|
||||
<string name="track_info_path">Percorso</string>
|
||||
<string name="track_info_size">Dimensione</string>
|
||||
<string name="track_info_suffix">Suffisso</string>
|
||||
<string name="track_info_summary_downloaded_file">Il file è stato scaricato utilizzando le API Subsonic. Il codec e il bitrate del file rimangono invariati rispetto al file sorgente.</string>
|
||||
<string name="track_info_summary_full_transcode">L\'applicazione richiederà al server di transcodedare il file e modificare il suo bitrate. Il codec richiesto dall\'utente è %1$s, con un bitrate di %2$s. Eventuali modifiche al codec e al bitrate del file nel formato scelto saranno gestite dal server, che potrebbe o meno supportare l\'operazione.</string>
|
||||
<string name="track_info_summary_original_file">L\'applicazione leggerà solo il file originale fornito dal server. L\'app richiederà esplicitamente al server il file non transcodedato con il bitrate della sorgente originale.</string>
|
||||
<string name="track_info_summary_server_prioritized">La qualità del file da riprodurre è lasciata alla decisione del server. L\'app non imporrà la scelta di codec e bitrate per eventuali transcoding.</string>
|
||||
<string name="track_info_summary_transcoding_bitrate">L\'applicazione richiederà al server di modificare il bitrate del file. L\'utente ha richiesto un bitrate di %1$s, mentre il codec del file sorgente rimarrà lo stesso. Eventuali modifiche al bitrate del file nel formato scelto saranno effettuate dal server, che potrebbe o meno supportare l\'operazione.</string>
|
||||
<string name="track_info_summary_transcoding_codec">L\'applicazione richiederà al server di transcodedare il file. Il codec richiesto dall\'utente è %1$s, mentre il bitrate sarà lo stesso del file sorgente. L\'eventuale transcoding del file nel formato scelto dipende dal server, in quanto potrebbe o meno supportare l\'operazione.</string>
|
||||
<string name="track_info_title">Titolo</string>
|
||||
<string name="track_info_track_number">Numero traccia</string>
|
||||
<string name="track_info_transcoded_content_type">Tipo di contenuto transcodedato</string>
|
||||
<string name="track_info_transcoded_suffix">Suffisso transcodedato</string>
|
||||
<string name="track_info_year">Anno</string>
|
||||
<string name="undraw_page">unDraw</string>
|
||||
<string name="undraw_thanks">Un ringraziamento speciale va a unDraw, senza le cui illustrazioni non avremmo potuto rendere questa applicazione più bella.</string>
|
||||
<string name="undraw_url">https://undraw.co/</string>
|
||||
<string name="player_lyrics_download_content_description">Scarica i testi delle canzoni per riprodurli offline</string>
|
||||
<string name="player_lyrics_downloaded_content_description">Testi scaricati per la riproduzione offline</string>
|
||||
<string name="player_lyrics_download_success">Testi salvati per la riproduzione offline.</string>
|
||||
<string name="player_lyrics_download_failure">I testi non sono disponibili per il download.</string>
|
||||
<string name="player_server_priority">Priorità server</string>
|
||||
<string name="player_unknown_format">Formato sconosciuto</string>
|
||||
<string name="player_transcoding">Transcodifica</string>
|
||||
<string name="player_transcoding_requested">richiesto</string>
|
||||
<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_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>
|
||||
<string name="playlist_chooser_dialog_toast_all_skipped">Tutte le canzoni sono state saltate perché duplicate</string>
|
||||
<string name="playlist_counted_tracks">%1$d brani • %2$s</string>
|
||||
<string name="playlist_duration">Durata • %1$s</string>
|
||||
<string name="playlist_editor_dialog_action_delete_toast">Premi a lungo per eliminare</string>
|
||||
<string name="playlist_editor_dialog_hint_name">Nome della playlist</string>
|
||||
<string name="playlist_editor_dialog_negative_button">Annulla</string>
|
||||
<string name="playlist_editor_dialog_neutral_button">Elimina</string>
|
||||
<string name="playlist_editor_dialog_positive_button">Salva</string>
|
||||
<string name="playlist_editor_dialog_title">Modifica playlist</string>
|
||||
<string name="playlist_page_play_button">Riproduci</string>
|
||||
<string name="playlist_page_shuffle_button">Mescola</string>
|
||||
<string name="playlist_song_count">Playlist • %1$d brani</string>
|
||||
<string name="podcast_bottom_sheet_add_to_queue">Aggiungi alla coda</string>
|
||||
<string name="podcast_bottom_sheet_delete">Elimina</string>
|
||||
<string name="podcast_bottom_sheet_download">Scarica</string>
|
||||
<string name="podcast_bottom_sheet_go_to_channel">Vai al canale</string>
|
||||
<string name="podcast_bottom_sheet_play_next">Riproduci dopo</string>
|
||||
<string name="podcast_bottom_sheet_remove">Rimuovi</string>
|
||||
<string name="podcast_channel_catalogue_title">Canali</string>
|
||||
<string name="podcast_channel_catalogue_title_expanded">Sfoglia Canali</string>
|
||||
<string name="podcast_channel_editor_dialog_hint_rss_url">URL RSS</string>
|
||||
<string name="podcast_channel_editor_dialog_title">Canale Podcast</string>
|
||||
<string name="podcast_channel_page_title_description_section">Descrizione</string>
|
||||
<string name="podcast_channel_page_title_episode_section">Episodi</string>
|
||||
<string name="podcast_channel_page_title_no_episode_available">Nessun episodio disponibile</string>
|
||||
<string name="podcast_episode_download_request_snackbar">La tua richiesta è stata inviata al server</string>
|
||||
<string name="podcast_info_empty_button">Clicca per nascondere la sezione\nGli effetti saranno visibili al riavvio</string>
|
||||
<string name="podcast_info_empty_subtitle">Una volta aggiunto un canale, lo troverai qui</string>
|
||||
<string name="podcast_info_empty_title">Nessun podcast trovato!</string>
|
||||
<string name="podcast_release_date_duration_formatter">%1$s • %2$s</string>
|
||||
<string name="radio_editor_dialog_hint_homepage_url">URL Homepage Radio</string>
|
||||
<string name="radio_editor_dialog_hint_name">Nome Radio</string>
|
||||
<string name="radio_editor_dialog_hint_stream_url">URL Stream Radio</string>
|
||||
<string name="radio_editor_dialog_negative_button">Annulla</string>
|
||||
<string name="radio_editor_dialog_neutral_button">Elimina</string>
|
||||
<string name="radio_editor_dialog_positive_button">Salva</string>
|
||||
<string name="radio_editor_dialog_title">Stazione Internet-Radio</string>
|
||||
<string name="radio_station_info_empty_button">Clicca per nascondere la sezione\nGli effetti saranno visibili al riavvio</string>
|
||||
<string name="radio_station_info_empty_subtitle">Una volta aggiunta una stazione radio, la troverai qui</string>
|
||||
<string name="radio_station_info_empty_title">Nessuna stazione trovata!</string>
|
||||
<string name="rating_dialog_negative_button">Annulla</string>
|
||||
<string name="rating_dialog_positive_button">Salva</string>
|
||||
<string name="rating_dialog_title">Valuta</string>
|
||||
<string name="search_hint">Cerca titolo, artisti o album</string>
|
||||
<string name="search_info_minimum_characters">Inserisci almeno tre caratteri</string>
|
||||
<string name="search_title_album">Album</string>
|
||||
<string name="search_title_artist">Artisti</string>
|
||||
<string name="search_title_song">Brani</string>
|
||||
<string name="server_signup_dialog_action_low_security">Bassa sicurezza</string>
|
||||
<string name="server_signup_dialog_action_delete_toast">Premi a lungo per eliminare</string>
|
||||
<string name="server_signup_dialog_hint_local_address">URL locale</string>
|
||||
<string name="server_signup_dialog_hint_name">Nome Server</string>
|
||||
<string name="server_signup_dialog_hint_password">Password</string>
|
||||
<string name="server_signup_dialog_hint_url">URL Server</string>
|
||||
<string name="server_signup_dialog_hint_username">Nome utente</string>
|
||||
<string name="server_signup_dialog_negative_button">Annulla</string>
|
||||
<string name="server_signup_dialog_neutral_button">Elimina</string>
|
||||
<string name="server_signup_dialog_positive_button">Salva</string>
|
||||
<string name="server_signup_dialog_title">Aggiungi server</string>
|
||||
<string name="server_unreachable_dialog_negative_button">Annulla</string>
|
||||
<string name="server_unreachable_dialog_neutral_button">Vai al login</string>
|
||||
<string name="server_unreachable_dialog_positive_button">Continua comunque</string>
|
||||
<string name="server_unreachable_dialog_summary">Il server richiesto non è disponibile. Se scegli di continuare, questo messaggio non apparirà per la prossima ora.</string>
|
||||
<string name="server_unreachable_dialog_title">Server irraggiungibile</string>
|
||||
<string name="settings_about_summary">Tempus è un client musicale open source e leggero per Subsonic, progettato e costruito nativamente per Android.</string>
|
||||
<string name="settings_about_title">Informazioni</string>
|
||||
<string name="settings_always_on_display">Sempre attivo</string>
|
||||
<string name="settings_allow_playlist_duplicates">Allow adding duplicates to playlist</string>
|
||||
<string name="settings_allow_playlist_duplicates_summary">If enabled, duplicates won\'t be checked while adding to a playlist.</string>
|
||||
<string name="settings_audio_transcode_download_format">Formato transcodifica</string>
|
||||
<string name="settings_audio_transcode_download_priority_summary">Se abilitato, Tempus non forzerà il download del brano con le impostazioni di transcodifica sottostanti.</string>
|
||||
<string name="settings_audio_transcode_download_priority_title">Dare priorità alle impostazioni del server per lo streaming nei download</string>
|
||||
<string name="settings_audio_transcode_download_summary">Se abilitato, Tempus scaricherà i brani transcodificati.</string>
|
||||
<string name="settings_audio_transcode_download_title">Scarica brani transcodificati</string>
|
||||
<string name="settings_audio_transcode_estimate_content_length_summary">Se abilitato, verrà richiesto al server di fornire la durata stimata del brano.</string>
|
||||
<string name="settings_audio_transcode_estimate_content_length_title">Stima della lunghezza del contenuto</string>
|
||||
<string name="settings_audio_transcode_format_download">Formato transcodifica per download</string>
|
||||
<string name="settings_audio_transcode_format_mobile">Formato transcodifica su mobile</string>
|
||||
<string name="settings_audio_transcode_format_wifi">Formato transcodifica su Wi-Fi</string>
|
||||
<string name="settings_audio_transcode_priority_summary">Se abilitato, Tempus non forzerà lo streaming del brano con le impostazioni di transcodifica sottostanti.</string>
|
||||
<string name="settings_audio_transcode_priority_title">Dare priorità alle impostazioni di transcodifica del server</string>
|
||||
<string name="settings_audio_transcode_priority_toast">Priorità di transcodifica del brano assegnata al server</string>
|
||||
<string name="settings_buffering_strategy">Strategia di buffering</string>
|
||||
<string name="settings_buffering_strategy_summary">Perché la modifica abbia effetto è necessario riavviare manualmente l\'app.</string>
|
||||
<string name="settings_choose_download_folder">Scegli una cartella dove scaricare la musica</string>
|
||||
<string name="settings_clear_download_folder">Svuota la cartella di download</string>
|
||||
<string name="settings_continuous_play_summary">Consente alla musica di continuare a suonare dopo la fine di una playlist, riproducendo brani simili</string>
|
||||
<string name="settings_continuous_play_title">Riproduzione continua</string>
|
||||
<string name="settings_covers_cache">Dimensione della cache delle copertine</string>
|
||||
<string name="settings_data_saving_mode_summary">Per ridurre il consumo di dati, evita di scaricare le copertine.</string>
|
||||
<string name="settings_data_saving_mode_title">Limita utilizzo dei dati mobili</string>
|
||||
<string name="settings_delete_download_storage_summary">Continuando, tutti gli elementi salvati verranno eliminati in modo irreversibile.</string>
|
||||
<string name="settings_delete_download_storage_title">Elimina elementi salvati</string>
|
||||
<string name="settings_download_storage_title">Archivio download</string>
|
||||
<string name="settings_download_folder_cleared">Cartella di download svuotata.</string>
|
||||
<string name="settings_download_folder_set">Cartella di download impostata</string>
|
||||
<string name="settings_set_download_folder">Imposta cartella di download</string>
|
||||
<string name="settings_system_equalizer_summary">Regola le impostazioni audio</string>
|
||||
<string name="settings_system_equalizer_title">Equalizzatore di sistema</string>
|
||||
<string name="settings_github_link">https://github.com/eddyizm/tempus</string>
|
||||
<string name="settings_github_summary">Segui lo sviluppo</string>
|
||||
<string name="settings_github_title">Github</string>
|
||||
<string name="settings_support_discussion_link">https://github.com/eddyizm/tempus/discussions</string>
|
||||
<string name="settings_github_update">Aggiornamenti</string>
|
||||
<string name="settings_github_update_title">Controlla GitHub per aggiornamenti</string>
|
||||
<string name="settings_github_update_summary">Se si utilizza la versione GitHub, per impostazione predefinita l\'app controllerà la presenza di nuove versioni. Disattiva per disabilitare i controlli automatici su GitHub</string>
|
||||
<string name="settings_support_summary">Partecipa alle discussioni della community e al supporto</string>
|
||||
<string name="settings_support_title">Supporto utenti</string>
|
||||
<string name="settings_scan_result">Scansione: conteggio di %1$d brani</string>
|
||||
<string name="settings_image_size">Imposta risoluzione delle immagini</string>
|
||||
<string name="settings_language">Lingua</string>
|
||||
<string name="settings_logout_title">Esci</string>
|
||||
<string name="settings_max_bitrate_download">Bitrate per download</string>
|
||||
<string name="settings_max_bitrate_mobile">Bitrate su mobile</string>
|
||||
<string name="settings_max_bitrate_wifi">Bitrate su Wi-Fi</string>
|
||||
<string name="settings_media_cache">Dimensione della cache dei file multimediali</string>
|
||||
<string name="settings_music_directory">Mostra directory musicali</string>
|
||||
<string name="settings_music_directory_summary">Se abilitato, mostra la sezione delle directory musicali. Nota che per la navigazione nelle cartelle è necessario che il server supporti questa funzionalità.</string>
|
||||
<string name="settings_podcast">Mostra podcast</string>
|
||||
<string name="settings_podcast_summary">Se abilitato, mostra la sezione podcast. Riavvia l\'app per rendere effettive le modifiche.</string>
|
||||
<string name="settings_audio_quality">Mostra qualità audio</string>
|
||||
<string name="settings_audio_quality_summary">Il bitrate e il formato audio saranno mostrati per ogni traccia.</string>
|
||||
<string name="settings_song_rating">Mostra valutazione della canzone</string>
|
||||
<string name="settings_song_rating_summary">Se abilitato, mostra la valutazione a 5 stelle per la traccia nella pagina della canzone\n\n*Richiede il riavvio dell\'app</string>
|
||||
<string name="settings_item_rating">Mostra valutazione</string>
|
||||
<string name="settings_item_rating_summary">Se abilitato, verrà mostrata la valutazione dell\'elemento e se è contrassegnato come preferito.</string>
|
||||
<string name="settings_queue_syncing_countdown">Timer sincronizzazione</string>
|
||||
<string name="settings_queue_syncing_summary">Se abilitato, l\'utente avrà la possibilità di salvare la propria coda di riproduzione e potrà caricare lo stato all\'apertura dell\'applicazione.</string>
|
||||
<string name="settings_queue_syncing_title">Sincronizza coda di riproduzione per questo utente [Not Fully Baked]</string>
|
||||
<string name="settings_show_mini_shuffle_button">Mostra il pulsante di riproduzione casuale</string>
|
||||
<string name="settings_show_mini_shuffle_button_summary">Se abilitato, mostra il pulsante di riproduzione casuale, rimuove il cuore nel mini player</string>
|
||||
<string name="settings_radio">Mostra radio</string>
|
||||
<string name="settings_radio_summary">Se abilitato, mostra la sezione radio. Riavvia l\'app per applicare completamente le modifiche.</string>
|
||||
<string name="settings_auto_download_lyrics">Scarica automaticamente i testi</string>
|
||||
<string name="settings_auto_download_lyrics_summary">Salva automaticamente i testi quando sono disponibili in modo che possano essere mostrati offline.</string>
|
||||
<string name="settings_replay_gain">Imposta modalità di guadagno di riproduzione</string>
|
||||
<string name="settings_rounded_corner">Angoli arrotondati</string>
|
||||
<string name="settings_rounded_corner_size">Dimensione angoli</string>
|
||||
<string name="settings_rounded_corner_size_summary">Imposta la grandezza dell\'angolo di curvatura.</string>
|
||||
<string name="settings_rounded_corner_summary">Se abilitato, imposta un angolo di curvatura per tutte le copertine visualizzate. Le modifiche avranno effetto al riavvio.</string>
|
||||
<string name="settings_scan_title">Scansiona libreria</string>
|
||||
<string name="settings_scrobble_title">Abilita scrobbling musicale</string>
|
||||
<string name="settings_system_language">Lingua di sistema</string>
|
||||
<string name="settings_share_title">Abilita condivisione musicale</string>
|
||||
<string name="settings_streaming_cache_size">Dimensione cache streaming</string>
|
||||
<string name="settings_streaming_cache_storage_title">Archiviazione cache streaming</string>
|
||||
<string name="settings_sub_summary_scrobble">È importante notare che lo scrobbling si basa anche sul fatto che il server sia abilitato a ricevere questi dati.</string>
|
||||
<string name="settings_summary_skip_min_star_rating">Quando si ascolta la radio di un artista, un mix istantaneo o quando si mescolano tutti i brani, i brani sotto una certa valutazione dell\'utente verranno ignorati.</string>
|
||||
<string name="settings_summary_replay_gain">Il guadagno di riproduzione è una funzionalità che consente di regolare il livello del volume delle tracce audio per un\'esperienza di ascolto coerente. Questa impostazione è efficace solo se la traccia contiene i metadati necessari.</string>
|
||||
<string name="settings_summary_scrobble">Lo scrobbling è una funzionalità che consente al tuo dispositivo di inviare informazioni sulle canzoni che ascolti al server musicale. Queste informazioni aiutano a creare raccomandazioni personalizzate in base alle tue preferenze musicali.</string>
|
||||
<string name="settings_summary_share">Permette all\'utente di condividere musica tramite un link. La funzionalità deve essere supportata e abilitata sul server ed è limitata a brani, album e playlist singoli.</string>
|
||||
<string name="settings_summary_syncing">Restituisce lo stato della coda di riproduzione per questo utente. Ciò include i brani nella coda di riproduzione, il brano attualmente in riproduzione e la posizione all\'interno di questo brano. Il server deve supportare questa funzionalità.\n*This setting is not 100% working on all servers/devices.</string>
|
||||
<string name="settings_summary_streaming_cache_size">%1$s \nAttualmente in uso: %2$s MiB</string>
|
||||
<string name="settings_summary_transcoding">Priorità data alla modalità di transcoding. Se impostato su "Riproduzione diretta", il bitrate del file non verrà modificato.</string>
|
||||
<string name="settings_summary_transcoding_download">Scarica media transcodificati. Se abilitato, l\'endpoint di download non verrà utilizzato, ma le impostazioni seguenti. \n\n Se "Formato di transcodifica per i download" è impostato su "Download diretto", il bitrate del file non verrà modificato.</string>
|
||||
<string name="settings_summary_transcoding_estimate_content_length">Quando il file viene transcodificato al volo, il client di solito non mostra la lunghezza della traccia. È possibile richiedere ai server che supportano la funzionalità di stimare la durata della traccia in riproduzione, ma i tempi di risposta possono essere più lunghi.</string>
|
||||
<string name="settings_sync_starred_artists_for_offline_use_summary">Se abilitato, gli artisti preferiti verranno scaricati per l\'uso offline.</string>
|
||||
<string name="settings_sync_starred_artists_for_offline_use_title">Sincronizza artisti preferiti per uso offline</string>
|
||||
<string name="settings_sync_starred_albums_for_offline_use_summary">Se abilitato, gli album preferiti verranno scaricati per l\'uso offline.</string>
|
||||
<string name="settings_sync_starred_albums_for_offline_use_title">Sincronizza album preferiti per uso offline</string>
|
||||
<string name="settings_sync_starred_tracks_for_offline_use_summary">Se abilitato, le tracce preferite verranno scaricate per l\'uso offline.</string>
|
||||
<string name="settings_sync_starred_tracks_for_offline_use_title">Sincronizza tracce preferite per uso offline</string>
|
||||
<string name="settings_theme">Tema</string>
|
||||
<string name="settings_title_data">Dati</string>
|
||||
<string name="settings_title_general">Generale</string>
|
||||
<string name="settings_title_playlist">Playlist</string>
|
||||
<string name="settings_title_rating">Valutazione</string>
|
||||
<string name="settings_title_replay_gain">Guadagno di riproduzione</string>
|
||||
<string name="settings_title_scrobble">Scrobble</string>
|
||||
<string name="settings_title_skip_min_star_rating">Ignora brani in base alla valutazione</string>
|
||||
<string name="settings_title_skip_min_star_rating_dialog">Brani con una valutazione di:</string>
|
||||
<string name="settings_title_share">Condividi</string>
|
||||
<string name="settings_title_syncing">Sincronizzazione</string>
|
||||
<string name="settings_title_transcoding">Transcodifica</string>
|
||||
<string name="settings_title_transcoding_download">Transcodifica dei Download</string>
|
||||
<string name="settings_title_ui">Interfaccia utente</string>
|
||||
<string name="settings_transcoded_download">Download transcodificato</string>
|
||||
<string name="settings_version_summary" translatable="false">3.1.0</string>
|
||||
<string name="settings_version_title">Versione</string>
|
||||
<string name="settings_wifi_only_summary">Chiedi conferma all\'utente prima di effettuare streaming su rete mobile.</string>
|
||||
<string name="settings_wifi_only_title">Streaming solo tramite Wi-Fi avviso</string>
|
||||
<string name="share_bottom_sheet_copy_link">Copia link</string>
|
||||
<string name="share_bottom_sheet_delete">Elimina condivisione</string>
|
||||
<string name="share_bottom_sheet_update">Aggiorna condivisione</string>
|
||||
<string name="share_subtitle_item">Data di scadenza: %1$s</string>
|
||||
<string name="share_no_expiration">Mai</string>
|
||||
<string name="share_unsupported_error">La condivisione non è supportata o non è abilitata</string>
|
||||
<string name="asset_link_clipboard_label">Link asset Tempus</string>
|
||||
<string name="asset_link_label_song">UID Canzone</string>
|
||||
<string name="asset_link_label_album">UID Album</string>
|
||||
<string name="asset_link_label_artist">UID Artista</string>
|
||||
<string name="asset_link_label_playlist">UID Playlist</string>
|
||||
<string name="asset_link_label_genre">UID Genere</string>
|
||||
<string name="asset_link_label_year">UID Anno</string>
|
||||
<string name="asset_link_label_unknown">UID Asset</string>
|
||||
<string name="asset_link_error_unsupported">Link asset non supportato</string>
|
||||
<string name="asset_link_error_song">Impossibile aprire la canzone</string>
|
||||
<string name="asset_link_error_album">Impossibile aprire l\'album</string>
|
||||
<string name="asset_link_error_artist">Impossibile aprire l\'artista</string>
|
||||
<string name="asset_link_error_playlist">Impossibile aprire la playlist</string>
|
||||
<string name="asset_link_chip_text">%1$s • %2$s</string>
|
||||
<string name="asset_link_copied_toast">Copiato %1$s negli appunti</string>
|
||||
<string name="asset_link_debug_toast">Link asset: %1$s</string>
|
||||
<string name="share_update_dialog_hint_description">Descrizione</string>
|
||||
<string name="share_update_dialog_hint_expiration_date">Data di scadenza</string>
|
||||
<string name="share_update_dialog_negative_button">Annulla</string>
|
||||
<string name="share_update_dialog_positive_button">Salva</string>
|
||||
<string name="share_update_dialog_title">Condividi</string>
|
||||
<string name="song_bottom_sheet_add_to_playlist">Aggiungi alla playlist</string>
|
||||
<string name="song_bottom_sheet_add_to_queue">Aggiungi alla coda</string>
|
||||
<string name="song_bottom_sheet_download">Scarica</string>
|
||||
<string name="song_bottom_sheet_error_retrieving_album">Errore nel recupero dell\'album</string>
|
||||
<string name="song_bottom_sheet_error_retrieving_artist">Errore nel recupero dell\'artista</string>
|
||||
<string name="song_bottom_sheet_go_to_album">Vai all\'album</string>
|
||||
<string name="song_bottom_sheet_go_to_artist">Vai all\'artista</string>
|
||||
<string name="song_bottom_sheet_instant_mix">Mix istantaneo</string>
|
||||
<string name="song_bottom_sheet_play_next">Riproduci dopo</string>
|
||||
<string name="song_bottom_sheet_rate">Valuta</string>
|
||||
<string name="song_bottom_sheet_remove">Rimuovi</string>
|
||||
<string name="song_bottom_sheet_share">Condividi</string>
|
||||
<string name="song_list_page_downloaded">Scaricato</string>
|
||||
<string name="song_list_page_most_played">Tracce più riprodotte</string>
|
||||
<string name="song_list_page_recently_added">Tracce aggiunte di recente</string>
|
||||
<string name="song_list_page_recently_played">Tracce riprodotte di recente</string>
|
||||
<string name="song_list_page_starred">Tracce contrassegnate</string>
|
||||
<string name="song_list_page_top">Le migliori tracce di %1$s</string>
|
||||
<string name="song_list_page_year">Anno %1$d</string>
|
||||
<string name="song_subtitle_formatter">%1$s • %2$s %3$s</string>
|
||||
<string name="starred_sync_dialog_negative_button">Annulla</string>
|
||||
<string name="starred_sync_dialog_neutral_button">Continua</string>
|
||||
<string name="starred_sync_dialog_positive_button">Continua e scarica</string>
|
||||
<string name="starred_sync_dialog_summary">Il download delle tracce contrassegnate potrebbe richiedere una grande quantità di dati.</string>
|
||||
<string name="starred_sync_dialog_title">Sincronizza tracce contrassegnate</string>
|
||||
<string name="starred_artist_sync_dialog_summary">Scaricare gli artisti preferiti potrebbe richiedere una grande quantità di dati.</string>
|
||||
<string name="starred_artist_sync_dialog_title">Sincronizza artisti preferiti</string>
|
||||
<string name="starred_album_sync_dialog_summary">Scaricare gli album preferiti potrebbe richiedere una grande quantità di dati.</string>
|
||||
<string name="starred_album_sync_dialog_title">Sincronizza album preferiti</string>
|
||||
<string name="streaming_cache_storage_dialog_sub_summary">Per rendere effettive le modifiche, riavvia l\'app.</string>
|
||||
<string name="streaming_cache_storage_dialog_summary">Cambiare la destinazione dei file memorizzati nella cache da un\'unità di archiviazione a un\'altra può comportare la cancellazione di eventuali file memorizzati nella cache in precedenza nell\'altra unità di archiviazione.</string>
|
||||
<string name="streaming_cache_storage_dialog_title">Seleziona opzione di archiviazione</string>
|
||||
<string name="streaming_cache_storage_external_dialog_positive_button">Esterno</string>
|
||||
<string name="streaming_cache_storage_internal_dialog_negative_button">Interno</string>
|
||||
<string name="support_url">https://ko-fi.com/eddyizm</string>
|
||||
<string name="track_info_album">Album</string>
|
||||
<string name="track_info_artist">Artista</string>
|
||||
<string name="track_info_bit_depth">Profondità bit</string>
|
||||
<string name="track_info_bitrate">Bitrate</string>
|
||||
<string name="track_info_content_type">Tipo di contenuto</string>
|
||||
<string name="track_info_dialog_positive_button">OK</string>
|
||||
<string name="track_info_dialog_title">Info traccia</string>
|
||||
<string name="track_info_disc_number">Numero del disco</string>
|
||||
<string name="track_info_duration">Durata</string>
|
||||
<string name="track_info_genre">Genere</string>
|
||||
<string name="track_info_path">Percorso</string>
|
||||
<string name="track_info_sampling_rate">Frequenza di campionamento</string>
|
||||
<string name="track_info_size">Dimensione</string>
|
||||
<string name="track_info_suffix">Suffisso</string>
|
||||
<string name="track_info_summary_downloaded_file">Il file è stato scaricato utilizzando le API Subsonic. Il codec e il bitrate del file rimangono invariati rispetto al file sorgente.</string>
|
||||
<string name="track_info_summary_full_transcode">L\'applicazione richiederà al server di transcodedare il file e modificare il suo bitrate. Il codec richiesto dall\'utente è %1$s, con un bitrate di %2$s. Eventuali modifiche al codec e al bitrate del file nel formato scelto saranno gestite dal server, che potrebbe o meno supportare l\'operazione.</string>
|
||||
<string name="track_info_summary_original_file">L\'applicazione leggerà solo il file originale fornito dal server. L\'app richiederà esplicitamente al server il file non transcodedato con il bitrate della sorgente originale.</string>
|
||||
<string name="track_info_summary_server_prioritized">La qualità del file da riprodurre è lasciata alla decisione del server. L\'app non imporrà la scelta di codec e bitrate per eventuali transcoding.</string>
|
||||
<string name="track_info_summary_transcoding_bitrate">L\'applicazione richiederà al server di modificare il bitrate del file. L\'utente ha richiesto un bitrate di %1$s, mentre il codec del file sorgente rimarrà lo stesso. Eventuali modifiche al bitrate del file nel formato scelto saranno effettuate dal server, che potrebbe o meno supportare l\'operazione.</string>
|
||||
<string name="track_info_summary_transcoding_codec">L\'applicazione richiederà al server di transcodedare il file. Il codec richiesto dall\'utente è %1$s, mentre il bitrate sarà lo stesso del file sorgente. L\'eventuale transcoding del file nel formato scelto dipende dal server, in quanto potrebbe o meno supportare l\'operazione.</string>
|
||||
<string name="track_info_title">Titolo</string>
|
||||
<string name="track_info_track_number">Numero traccia</string>
|
||||
<string name="track_info_transcoded_content_type">Tipo di contenuto transcodificato</string>
|
||||
<string name="track_info_transcoded_suffix">Suffisso transcodificato</string>
|
||||
<string name="track_info_year">Anno</string>
|
||||
<string name="undraw_page">unDraw</string>
|
||||
<string name="undraw_thanks">Un ringraziamento speciale va a unDraw, senza le cui illustrazioni non avremmo potuto rendere questa applicazione più bella.</string>
|
||||
<string name="undraw_url">https://undraw.co/</string>
|
||||
<string name="widget_label">Widget Tempus</string>
|
||||
<string name="widget_not_playing">Non in riproduzione</string>
|
||||
<string name="widget_placeholder_subtitle">Apri Tempus</string>
|
||||
<string name="widget_time_elapsed_placeholder">0:00</string>
|
||||
<string name="widget_time_duration_placeholder">0:00</string>
|
||||
<string name="widget_content_desc_album_art">Immagine dell\'album</string>
|
||||
<string name="widget_content_desc_play_pause">Riproduci o metti in pausa</string>
|
||||
<string name="widget_content_desc_next">Traccia successiva</string>
|
||||
<string name="widget_content_desc_prev">Traccia precedente</string>
|
||||
<string name="widget_content_desc_shuffle">Attiva/disattiva riproduzione casuale</string>
|
||||
<string name="widget_content_desc_repeat">Cambia modalità di ripetizione</string>
|
||||
<plurals name="home_sync_starred_albums_count">
|
||||
<item quantity="one">%d album da sincronizzare</item>
|
||||
<item quantity="other">%d album da sincronizzare</item>
|
||||
</plurals>
|
||||
<plurals name="home_sync_starred_artists_count">
|
||||
<item quantity="one">%d artista da sincronizzare</item>
|
||||
<item quantity="other">%d artisti da sincronizzare</item>
|
||||
</plurals>
|
||||
<plurals name="songs_download_started">
|
||||
<item quantity="one">Scaricando %d canzone</item>
|
||||
<item quantity="other">Scaricando %d canzoni</item>
|
||||
</plurals>
|
||||
<string name="equalizer_fragment_title">Equalizzatore</string>
|
||||
<string name="equalizer_reset">Reimposta</string>
|
||||
<string name="equalizer_enable">Abilita</string>
|
||||
<string name="equalizer_not_supported">Non supportato su questo dispositivo</string>
|
||||
<string name="settings_app_equalizer">Equalizzatore</string>
|
||||
<string name="settings_app_equalizer_summary">Apri l\'equalizzatore integrato</string>
|
||||
|
||||
<string name="settings_album_detail">Mostra dettagli album</string>
|
||||
<string name="settings_album_detail_summary">Se abilitato, mostra i dettagli dell\'album come genere, numero di canzoni, ecc. nella pagina dell\'album</string>
|
||||
<string name="settings_artist_sort_by_album_count">Ordina artisti per numero di album</string>
|
||||
<string name="settings_artist_sort_by_album_count_summary">Se abilitato, ordina gli artisti per numero di album. Ordina per nome se disabilitato.</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -1,182 +1,18 @@
|
|||
package com.cappielloantonio.tempo.service
|
||||
|
||||
import android.app.PendingIntent.FLAG_IMMUTABLE
|
||||
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
|
||||
import android.app.TaskStackBuilder
|
||||
import android.content.Intent
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.Network
|
||||
import android.net.NetworkCapabilities
|
||||
import android.os.Binder
|
||||
import android.os.IBinder
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.media3.cast.CastPlayer
|
||||
import androidx.media3.cast.SessionAvailabilityListener
|
||||
import androidx.media3.common.AudioAttributes
|
||||
import androidx.media3.common.C
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.common.Tracks
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.exoplayer.DefaultLoadControl
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import androidx.media3.session.MediaLibraryService
|
||||
import androidx.media3.session.MediaSession.ControllerInfo
|
||||
import com.cappielloantonio.tempo.repository.AutomotiveRepository
|
||||
import com.cappielloantonio.tempo.repository.QueueRepository
|
||||
import com.cappielloantonio.tempo.ui.activity.MainActivity
|
||||
import com.cappielloantonio.tempo.util.AssetLinkUtil
|
||||
import com.cappielloantonio.tempo.util.Constants
|
||||
import com.cappielloantonio.tempo.util.DownloadUtil
|
||||
import com.cappielloantonio.tempo.util.DynamicMediaSourceFactory
|
||||
import com.cappielloantonio.tempo.util.MappingUtil
|
||||
import com.cappielloantonio.tempo.util.Preferences
|
||||
import com.cappielloantonio.tempo.util.ReplayGainUtil
|
||||
import com.cappielloantonio.tempo.widget.WidgetUpdateManager
|
||||
import com.google.android.gms.cast.framework.CastContext
|
||||
import com.google.android.gms.common.ConnectionResult
|
||||
import com.google.android.gms.common.GoogleApiAvailability
|
||||
|
||||
@UnstableApi
|
||||
class MediaService : MediaLibraryService(), SessionAvailabilityListener {
|
||||
private lateinit var automotiveRepository: AutomotiveRepository
|
||||
private lateinit var player: ExoPlayer
|
||||
class MediaService : BaseMediaService(), SessionAvailabilityListener {
|
||||
private val automotiveRepository = AutomotiveRepository()
|
||||
private lateinit var castPlayer: CastPlayer
|
||||
private lateinit var mediaLibrarySession: MediaLibrarySession
|
||||
private lateinit var librarySessionCallback: MediaLibrarySessionCallback
|
||||
private lateinit var networkCallback: CustomNetworkCallback
|
||||
lateinit var equalizerManager: EqualizerManager
|
||||
|
||||
inner class LocalBinder : Binder() {
|
||||
fun getEqualizerManager(): EqualizerManager {
|
||||
return this@MediaService.equalizerManager
|
||||
}
|
||||
}
|
||||
|
||||
private val binder = LocalBinder()
|
||||
|
||||
companion object {
|
||||
const val ACTION_BIND_EQUALIZER = "com.cappielloantonio.tempo.service.BIND_EQUALIZER"
|
||||
const val ACTION_EQUALIZER_UPDATED = "com.cappielloantonio.tempo.service.EQUALIZER_UPDATED"
|
||||
}
|
||||
private val widgetUpdateHandler = Handler(Looper.getMainLooper())
|
||||
private var widgetUpdateScheduled = false
|
||||
private val widgetUpdateRunnable = object : Runnable {
|
||||
override fun run() {
|
||||
if (!player.isPlaying) {
|
||||
widgetUpdateScheduled = false
|
||||
return
|
||||
}
|
||||
updateWidget()
|
||||
widgetUpdateHandler.postDelayed(this, WIDGET_UPDATE_INTERVAL_MS)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateMediaItems() {
|
||||
Log.d("MediaService", "update items");
|
||||
val n = player.mediaItemCount
|
||||
val k = player.currentMediaItemIndex
|
||||
val current = player.currentPosition
|
||||
val items = (0 .. n-1).map{i -> MappingUtil.mapMediaItem(player.getMediaItemAt(i))}
|
||||
player.clearMediaItems()
|
||||
player.setMediaItems(items, k, current)
|
||||
}
|
||||
|
||||
inner class CustomNetworkCallback : ConnectivityManager.NetworkCallback() {
|
||||
var wasWifi = false
|
||||
|
||||
init {
|
||||
val manager = getSystemService(ConnectivityManager::class.java)
|
||||
val network = manager.activeNetwork
|
||||
val capabilities = manager.getNetworkCapabilities(network)
|
||||
if (capabilities != null)
|
||||
wasWifi = capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
|
||||
}
|
||||
|
||||
override fun onCapabilitiesChanged(network : Network, networkCapabilities : NetworkCapabilities) {
|
||||
val isWifi = networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
|
||||
if (isWifi != wasWifi) {
|
||||
wasWifi = isWifi
|
||||
widgetUpdateHandler.post(Runnable {
|
||||
updateMediaItems()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
initializeRepository()
|
||||
initializePlayer()
|
||||
initializeMediaLibrarySession()
|
||||
restorePlayerFromQueue()
|
||||
initializePlayerListener()
|
||||
initializeCastPlayer()
|
||||
initializeEqualizerManager()
|
||||
initializeNetworkListener()
|
||||
|
||||
setPlayer(
|
||||
null,
|
||||
if (this::castPlayer.isInitialized && castPlayer.isCastSessionAvailable) castPlayer else player
|
||||
)
|
||||
}
|
||||
|
||||
override fun onGetSession(controllerInfo: ControllerInfo): MediaLibrarySession {
|
||||
return mediaLibrarySession
|
||||
}
|
||||
|
||||
override fun onTaskRemoved(rootIntent: Intent?) {
|
||||
val player = mediaLibrarySession.player
|
||||
|
||||
if (!player.playWhenReady || player.mediaItemCount == 0) {
|
||||
stopSelf()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
releaseNetworkCallback()
|
||||
equalizerManager.release()
|
||||
stopWidgetUpdates()
|
||||
releasePlayer()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? {
|
||||
// Check if the intent is for our custom equalizer binder
|
||||
if (intent?.action == ACTION_BIND_EQUALIZER) {
|
||||
return binder
|
||||
}
|
||||
// Otherwise, handle it as a normal MediaLibraryService connection
|
||||
return super.onBind(intent)
|
||||
}
|
||||
|
||||
private fun initializeRepository() {
|
||||
automotiveRepository = AutomotiveRepository()
|
||||
}
|
||||
|
||||
private fun initializeEqualizerManager() {
|
||||
equalizerManager = EqualizerManager()
|
||||
val audioSessionId = player.audioSessionId
|
||||
attachEqualizerIfPossible(audioSessionId)
|
||||
}
|
||||
|
||||
private fun initializePlayer() {
|
||||
player = ExoPlayer.Builder(this)
|
||||
.setRenderersFactory(getRenderersFactory())
|
||||
.setMediaSourceFactory(DynamicMediaSourceFactory(this))
|
||||
.setAudioAttributes(AudioAttributes.DEFAULT, true)
|
||||
.setHandleAudioBecomingNoisy(true)
|
||||
.setWakeMode(C.WAKE_MODE_NETWORK)
|
||||
.setLoadControl(initializeLoadControl())
|
||||
.build()
|
||||
|
||||
player.shuffleModeEnabled = Preferences.isShuffleModeEnabled()
|
||||
player.repeatMode = Preferences.getRepeatMode()
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private fun initializeCastPlayer() {
|
||||
|
|
@ -184,284 +20,41 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
|
|||
.isGooglePlayServicesAvailable(this) == ConnectionResult.SUCCESS
|
||||
) {
|
||||
CastContext.getSharedInstance(this, ContextCompat.getMainExecutor(this))
|
||||
.addOnSuccessListener { castContext ->
|
||||
castPlayer = CastPlayer(castContext)
|
||||
castPlayer.setSessionAvailabilityListener(this@MediaService)
|
||||
|
||||
if (castPlayer.isCastSessionAvailable && this::mediaLibrarySession.isInitialized) {
|
||||
setPlayer(player, castPlayer)
|
||||
}
|
||||
}
|
||||
.addOnSuccessListener { castContext ->
|
||||
castPlayer = CastPlayer(castContext)
|
||||
castPlayer.setSessionAvailabilityListener(this@MediaService)
|
||||
initializePlayerListener(castPlayer)
|
||||
if (castPlayer.isCastSessionAvailable)
|
||||
setPlayer(mediaLibrarySession.player, castPlayer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun initializeMediaLibrarySession() {
|
||||
val sessionActivityPendingIntent =
|
||||
TaskStackBuilder.create(this).run {
|
||||
addNextIntent(Intent(this@MediaService, MainActivity::class.java))
|
||||
getPendingIntent(0, FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT)
|
||||
}
|
||||
|
||||
librarySessionCallback = createLibrarySessionCallback()
|
||||
mediaLibrarySession =
|
||||
MediaLibrarySession.Builder(this, player, librarySessionCallback)
|
||||
.setSessionActivity(sessionActivityPendingIntent)
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun initializeNetworkListener() {
|
||||
networkCallback = CustomNetworkCallback()
|
||||
getSystemService(ConnectivityManager::class.java).registerDefaultNetworkCallback(networkCallback)
|
||||
updateMediaItems()
|
||||
}
|
||||
|
||||
private fun restorePlayerFromQueue() {
|
||||
if (player.mediaItemCount > 0) return
|
||||
|
||||
val queueRepository = QueueRepository()
|
||||
val storedQueue = queueRepository.media
|
||||
if (storedQueue.isNullOrEmpty()) return
|
||||
|
||||
val mediaItems = MappingUtil.mapMediaItems(storedQueue)
|
||||
if (mediaItems.isEmpty()) return
|
||||
|
||||
val lastIndex = try {
|
||||
queueRepository.lastPlayedMediaIndex
|
||||
} catch (_: Exception) {
|
||||
0
|
||||
}.coerceIn(0, mediaItems.size - 1)
|
||||
|
||||
val lastPosition = try {
|
||||
queueRepository.lastPlayedMediaTimestamp
|
||||
} catch (_: Exception) {
|
||||
0L
|
||||
}.let { if (it < 0L) 0L else it }
|
||||
|
||||
player.setMediaItems(mediaItems, lastIndex, lastPosition)
|
||||
player.prepare()
|
||||
updateWidget()
|
||||
}
|
||||
|
||||
private fun createLibrarySessionCallback(): MediaLibrarySessionCallback {
|
||||
override fun getMediaLibrarySessionCallback(): MediaLibrarySession.Callback {
|
||||
return MediaLibrarySessionCallback(this, automotiveRepository)
|
||||
}
|
||||
|
||||
private fun initializePlayerListener() {
|
||||
player.addListener(object : Player.Listener {
|
||||
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
|
||||
if (mediaItem == null) return
|
||||
override fun playerInitHook() {
|
||||
super.playerInitHook()
|
||||
initializeCastPlayer()
|
||||
if (this::castPlayer.isInitialized && castPlayer.isCastSessionAvailable)
|
||||
setPlayer(null, castPlayer)
|
||||
}
|
||||
|
||||
if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_SEEK || reason == Player.MEDIA_ITEM_TRANSITION_REASON_AUTO) {
|
||||
MediaManager.setLastPlayedTimestamp(mediaItem)
|
||||
}
|
||||
updateWidget()
|
||||
}
|
||||
|
||||
override fun onTracksChanged(tracks: Tracks) {
|
||||
ReplayGainUtil.setReplayGain(player, tracks)
|
||||
|
||||
val currentMediaItem = player.currentMediaItem
|
||||
if (currentMediaItem != null && currentMediaItem.mediaMetadata.extras != null) {
|
||||
MediaManager.scrobble(currentMediaItem, false)
|
||||
}
|
||||
|
||||
if (player.currentMediaItemIndex + 1 == player.mediaItemCount)
|
||||
MediaManager.continuousPlay(player.currentMediaItem)
|
||||
}
|
||||
|
||||
override fun onIsPlayingChanged(isPlaying: Boolean) {
|
||||
if (!isPlaying) {
|
||||
MediaManager.setPlayingPausedTimestamp(
|
||||
player.currentMediaItem,
|
||||
player.currentPosition
|
||||
)
|
||||
} else {
|
||||
MediaManager.scrobble(player.currentMediaItem, false)
|
||||
}
|
||||
if (isPlaying) {
|
||||
scheduleWidgetUpdates()
|
||||
} else {
|
||||
stopWidgetUpdates()
|
||||
}
|
||||
updateWidget()
|
||||
}
|
||||
|
||||
override fun onPlaybackStateChanged(playbackState: Int) {
|
||||
super.onPlaybackStateChanged(playbackState)
|
||||
|
||||
if (!player.hasNextMediaItem() &&
|
||||
playbackState == Player.STATE_ENDED &&
|
||||
player.mediaMetadata.extras?.getString("type") == Constants.MEDIA_TYPE_MUSIC
|
||||
) {
|
||||
MediaManager.scrobble(player.currentMediaItem, true)
|
||||
MediaManager.saveChronology(player.currentMediaItem)
|
||||
}
|
||||
updateWidget()
|
||||
}
|
||||
|
||||
override fun onPositionDiscontinuity(
|
||||
oldPosition: Player.PositionInfo,
|
||||
newPosition: Player.PositionInfo,
|
||||
reason: Int
|
||||
) {
|
||||
super.onPositionDiscontinuity(oldPosition, newPosition, reason)
|
||||
|
||||
if (reason == Player.DISCONTINUITY_REASON_AUTO_TRANSITION) {
|
||||
if (oldPosition.mediaItem?.mediaMetadata?.extras?.getString("type") == Constants.MEDIA_TYPE_MUSIC) {
|
||||
MediaManager.scrobble(oldPosition.mediaItem, true)
|
||||
MediaManager.saveChronology(oldPosition.mediaItem)
|
||||
}
|
||||
|
||||
if (newPosition.mediaItem?.mediaMetadata?.extras?.getString("type") == Constants.MEDIA_TYPE_MUSIC) {
|
||||
MediaManager.setLastPlayedTimestamp(newPosition.mediaItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onShuffleModeEnabledChanged(shuffleModeEnabled: Boolean) {
|
||||
Preferences.setShuffleModeEnabled(shuffleModeEnabled)
|
||||
}
|
||||
|
||||
override fun onRepeatModeChanged(repeatMode: Int) {
|
||||
Preferences.setRepeatMode(repeatMode)
|
||||
}
|
||||
|
||||
override fun onAudioSessionIdChanged(audioSessionId: Int) {
|
||||
attachEqualizerIfPossible(audioSessionId)
|
||||
}
|
||||
})
|
||||
if (player.isPlaying) {
|
||||
scheduleWidgetUpdates()
|
||||
override fun releasePlayers() {
|
||||
if (this::castPlayer.isInitialized) {
|
||||
castPlayer.setSessionAvailabilityListener(null)
|
||||
castPlayer.release()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateWidget() {
|
||||
val mi = player.currentMediaItem
|
||||
val title = mi?.mediaMetadata?.title?.toString()
|
||||
?: mi?.mediaMetadata?.extras?.getString("title")
|
||||
val artist = mi?.mediaMetadata?.artist?.toString()
|
||||
?: mi?.mediaMetadata?.extras?.getString("artist")
|
||||
val album = mi?.mediaMetadata?.albumTitle?.toString()
|
||||
?: mi?.mediaMetadata?.extras?.getString("album")
|
||||
val extras = mi?.mediaMetadata?.extras
|
||||
val coverId = extras?.getString("coverArtId")
|
||||
val songLink = extras?.getString("assetLinkSong")
|
||||
?: AssetLinkUtil.buildLink(AssetLinkUtil.TYPE_SONG, extras?.getString("id"))
|
||||
val albumLink = extras?.getString("assetLinkAlbum")
|
||||
?: AssetLinkUtil.buildLink(AssetLinkUtil.TYPE_ALBUM, extras?.getString("albumId"))
|
||||
val artistLink = extras?.getString("assetLinkArtist")
|
||||
?: AssetLinkUtil.buildLink(AssetLinkUtil.TYPE_ARTIST, extras?.getString("artistId"))
|
||||
val position = player.currentPosition.takeIf { it != C.TIME_UNSET } ?: 0L
|
||||
val duration = player.duration.takeIf { it != C.TIME_UNSET } ?: 0L
|
||||
WidgetUpdateManager.updateFromState(
|
||||
this,
|
||||
title ?: "",
|
||||
artist ?: "",
|
||||
album ?: "",
|
||||
coverId,
|
||||
player.isPlaying,
|
||||
player.shuffleModeEnabled,
|
||||
player.repeatMode,
|
||||
position,
|
||||
duration,
|
||||
songLink,
|
||||
albumLink,
|
||||
artistLink
|
||||
)
|
||||
}
|
||||
|
||||
private fun scheduleWidgetUpdates() {
|
||||
if (widgetUpdateScheduled) return
|
||||
widgetUpdateHandler.postDelayed(widgetUpdateRunnable, WIDGET_UPDATE_INTERVAL_MS)
|
||||
widgetUpdateScheduled = true
|
||||
}
|
||||
|
||||
private fun stopWidgetUpdates() {
|
||||
if (!widgetUpdateScheduled) return
|
||||
widgetUpdateHandler.removeCallbacks(widgetUpdateRunnable)
|
||||
widgetUpdateScheduled = false
|
||||
}
|
||||
|
||||
private fun initializeLoadControl(): DefaultLoadControl {
|
||||
return DefaultLoadControl.Builder()
|
||||
.setBufferDurationsMs(
|
||||
(DefaultLoadControl.DEFAULT_MIN_BUFFER_MS * Preferences.getBufferingStrategy()).toInt(),
|
||||
(DefaultLoadControl.DEFAULT_MAX_BUFFER_MS * Preferences.getBufferingStrategy()).toInt(),
|
||||
DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS,
|
||||
DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS
|
||||
)
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun getQueueFromPlayer(player: Player): List<MediaItem> {
|
||||
val queue = mutableListOf<MediaItem>()
|
||||
for (i in 0 until player.mediaItemCount) {
|
||||
queue.add(player.getMediaItemAt(i))
|
||||
}
|
||||
return queue
|
||||
}
|
||||
|
||||
private fun setPlayer(oldPlayer: Player?, newPlayer: Player) {
|
||||
if (oldPlayer === newPlayer) return
|
||||
oldPlayer?.stop()
|
||||
mediaLibrarySession.player = newPlayer
|
||||
}
|
||||
|
||||
private fun releasePlayer() {
|
||||
if (this::castPlayer.isInitialized) castPlayer.setSessionAvailabilityListener(null)
|
||||
if (this::castPlayer.isInitialized) castPlayer.release()
|
||||
player.release()
|
||||
mediaLibrarySession.release()
|
||||
automotiveRepository.deleteMetadata()
|
||||
super.releasePlayers()
|
||||
}
|
||||
|
||||
private fun releaseNetworkCallback() {
|
||||
getSystemService(ConnectivityManager::class.java).unregisterNetworkCallback(networkCallback)
|
||||
}
|
||||
|
||||
private fun getRenderersFactory() = DownloadUtil.buildRenderersFactory(this, false)
|
||||
|
||||
override fun onCastSessionAvailable() {
|
||||
val currentQueue = getQueueFromPlayer(player)
|
||||
val currentIndex = player.currentMediaItemIndex
|
||||
val currentPosition = player.currentPosition
|
||||
val isPlaying = player.playWhenReady
|
||||
|
||||
setPlayer(player, castPlayer)
|
||||
|
||||
castPlayer.setMediaItems(currentQueue, currentIndex, currentPosition)
|
||||
castPlayer.playWhenReady = isPlaying
|
||||
castPlayer.prepare()
|
||||
setPlayer(exoplayer, castPlayer)
|
||||
}
|
||||
|
||||
override fun onCastSessionUnavailable() {
|
||||
val currentQueue = getQueueFromPlayer(castPlayer)
|
||||
val currentIndex = castPlayer.currentMediaItemIndex
|
||||
val currentPosition = castPlayer.currentPosition
|
||||
val isPlaying = castPlayer.playWhenReady
|
||||
|
||||
setPlayer(castPlayer, player)
|
||||
|
||||
player.setMediaItems(currentQueue, currentIndex, currentPosition)
|
||||
player.playWhenReady = isPlaying
|
||||
player.prepare()
|
||||
setPlayer(castPlayer, exoplayer)
|
||||
}
|
||||
|
||||
private fun attachEqualizerIfPossible(audioSessionId: Int): Boolean {
|
||||
if (audioSessionId == 0 || audioSessionId == -1) return false
|
||||
val attached = equalizerManager.attachToSession(audioSessionId)
|
||||
if (attached) {
|
||||
val enabled = Preferences.isEqualizerEnabled()
|
||||
equalizerManager.setEnabled(enabled)
|
||||
val bands = equalizerManager.getNumberOfBands()
|
||||
val savedLevels = Preferences.getEqualizerBandLevels(bands)
|
||||
for (i in 0 until bands) {
|
||||
equalizerManager.setBandLevel(i.toShort(), savedLevels[i])
|
||||
}
|
||||
sendBroadcast(Intent(ACTION_EQUALIZER_UPDATED))
|
||||
}
|
||||
return attached
|
||||
}
|
||||
}
|
||||
|
||||
private const val WIDGET_UPDATE_INTERVAL_MS = 1000L
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue