Merge branch 'development' into library-play

This commit is contained in:
eddyizm 2025-11-22 14:30:20 -08:00
commit 3496918ce6
No known key found for this signature in database
GPG key ID: CF5F671829E8158A
14 changed files with 1237 additions and 1449 deletions

View file

@ -1,8 +1,20 @@
# Changelog # Changelog
## Pending release... ## Pending release...
* feat: Add Catalan i18n (#268)
* Fix player queue soft-lock (#266) ## [4.2.6](https://github.com/eddyizm/tempo/releases/tag/v4.2.6) (2025-11-22)
## What's Changed
* fix: Fix player queue soft-lock by @shrapnelnet in https://github.com/eddyizm/tempus/pull/266
* chore: Add Catalan i18n by @marcriera in https://github.com/eddyizm/tempus/pull/268
* chore: Refactor MediaService by @pca006132 in https://github.com/eddyizm/tempus/pull/267
* chore(i18n): Update Spanish translation by @jaime-grj in https://github.com/eddyizm/tempus/pull/272
* chore(i18n): Update Italian translation by @66Bunz in https://github.com/eddyizm/tempus/pull/278
## New Contributors
* @marcriera made their first contribution in https://github.com/eddyizm/tempus/pull/268
* @66Bunz made their first contribution in https://github.com/eddyizm/tempus/pull/278
**Full Changelog**: https://github.com/eddyizm/tempus/compare/v4.2.4...v4.2.6
## [4.2.4](https://github.com/eddyizm/tempo/releases/tag/v4.2.4) (2025-11-15) ## [4.2.4](https://github.com/eddyizm/tempo/releases/tag/v4.2.4) (2025-11-15)
## What's Changed ## What's Changed

View file

@ -11,13 +11,14 @@
<div align="center"> <div align="center">
<!-- Reproducible build --> <!-- Reproducible build -->
<!-- [<img src="https://shields.rbtlog.dev/simple/com.eddyizm.degoogled.tempus" alt="RB Status">](https://shields.rbtlog.dev/com.eddyizm.degoogled.tempus) --> [<img src="https://shields.rbtlog.dev/simple/com.eddyizm.degoogled.tempus" alt="RB Status">](https://shields.rbtlog.dev/com.eddyizm.degoogled.tempus)
</div> </div>
<p align="center"> <p align="center">
<a href="https://github.com/eddyizm/tempus/releases"><img src="https://i.ibb.co/q0mdc4Z/get-it-on-github.png" width="200"></a> <a href="https://github.com/eddyizm/tempus/releases"><img src="https://i.ibb.co/q0mdc4Z/get-it-on-github.png" width="200"></a>
<a href="https://apt.izzysoft.de/fdroid/index/apk/com.eddyizm.degoogled.tempus"><img src="https://gitlab.com/IzzyOnDroid/repo/-/raw/master/assets/IzzyOnDroid.png" width="200"></a> <a href="https://apt.izzysoft.de/fdroid/index/apk/com.eddyizm.degoogled.tempus"><img src="https://gitlab.com/IzzyOnDroid/repo/-/raw/master/assets/IzzyOnDroid.png" width="200"></a>
<a href="https://apps.obtainium.imranr.dev/redirect?r=obtainium://app/%7B%22id%22%3A%22com.eddyizm.tempus%22%2C%22url%22%3A%22https%3A%2F%2Fgithub.com%2Feddyizm%2Ftempus%22%2C%22author%22%3A%22eddyizm%22%2C%22name%22%3A%22Tempus%22%2C%22preferredApkIndex%22%3A0%2C%22additionalSettings%22%3A%22%7B%5C%22includePrereleases%5C%22%3Afalse%2C%5C%22fallbackToOlderReleases%5C%22%3Atrue%2C%5C%22filterReleaseTitlesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22filterReleaseNotesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22verifyLatestTag%5C%22%3Afalse%2C%5C%22sortMethodChoice%5C%22%3A%5C%22date%5C%22%2C%5C%22useLatestAssetDateAsReleaseDate%5C%22%3Afalse%2C%5C%22releaseTitleAsVersion%5C%22%3Afalse%2C%5C%22trackOnly%5C%22%3Afalse%2C%5C%22versionExtractionRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22matchGroupToUse%5C%22%3A%5C%22%5C%22%2C%5C%22versionDetection%5C%22%3Atrue%2C%5C%22releaseDateAsVersion%5C%22%3Afalse%2C%5C%22useVersionCodeAsOSVersion%5C%22%3Afalse%2C%5C%22apkFilterRegEx%5C%22%3A%5C%22tempus%5C%22%2C%5C%22invertAPKFilter%5C%22%3Afalse%2C%5C%22autoApkFilterByArch%5C%22%3Atrue%2C%5C%22appName%5C%22%3A%5C%22%5C%22%2C%5C%22appAuthor%5C%22%3A%5C%22%5C%22%2C%5C%22shizukuPretendToBeGooglePlay%5C%22%3Afalse%2C%5C%22allowInsecure%5C%22%3Afalse%2C%5C%22exemptFromBackgroundUpdates%5C%22%3Afalse%2C%5C%22skipUpdateNotifications%5C%22%3Afalse%2C%5C%22about%5C%22%3A%5C%22%5C%22%2C%5C%22refreshBeforeDownload%5C%22%3Afalse%2C%5C%22includeZips%5C%22%3Afalse%2C%5C%22zippedApkFilterRegEx%5C%22%3A%5C%22%5C%22%7D%22%2C%22overrideSource%22%3A%22GitHub%22%7D"><img width="200" src="https://github.com/user-attachments/assets/119e7ff4-2636-43cb-ab7f-1b6a58ac3570" /></a>
</p> </p>
<!-- <!--
<a href="https://f-droid.org/packages/com.cappielloantonio.notquitemy.tempo"><img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png" width="200"></a> <a href="https://f-droid.org/packages/com.cappielloantonio.notquitemy.tempo"><img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png" width="200"></a>
@ -123,4 +124,4 @@ Tempus is released under the [GNU General Public License v3.0](LICENSE). Feel fr
## Credits ## Credits
Thanks to the original repo/creator [CappielloAntonio](https://github.com/CappielloAntonio) (forked from v3.9.0) Thanks to the original repo/creator [CappielloAntonio](https://github.com/CappielloAntonio) (forked from v3.9.0)
[Opensvg.org](https://opensvg.org) for the new turntable logo. [Opensvg.org](https://opensvg.org) for the new turntable logo.

View file

@ -10,8 +10,8 @@ android {
minSdkVersion 24 minSdkVersion 24
targetSdk 35 targetSdk 35
versionCode 6 versionCode 7
versionName '4.2.4' versionName '4.2.6'
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
javaCompileOptions { javaCompileOptions {

View file

@ -1,561 +1,6 @@
package com.cappielloantonio.tempo.service 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.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 @UnstableApi
class MediaService : MediaLibraryService() { class MediaService : BaseMediaService()
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

View file

@ -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

View file

@ -36,10 +36,12 @@ import com.google.common.util.concurrent.MoreExecutors;
import java.lang.ref.WeakReference; import java.lang.ref.WeakReference;
import java.util.List; import java.util.List;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicBoolean;
public class MediaManager { public class MediaManager {
private static final String TAG = "MediaManager"; private static final String TAG = "MediaManager";
private static WeakReference<MediaBrowser> attachedBrowserRef = new WeakReference<>(null); private static WeakReference<MediaBrowser> attachedBrowserRef = new WeakReference<>(null);
public static AtomicBoolean justStarted = new AtomicBoolean(false);
public static void registerPlaybackObserver( public static void registerPlaybackObserver(
ListenableFuture<MediaBrowser> browserFuture, ListenableFuture<MediaBrowser> browserFuture,
@ -179,8 +181,8 @@ public class MediaManager {
try { try {
if (mediaBrowserListenableFuture.isDone()) { if (mediaBrowserListenableFuture.isDone()) {
MediaBrowser browser = mediaBrowserListenableFuture.get(); MediaBrowser browser = mediaBrowserListenableFuture.get();
browser.clearMediaItems(); justStarted.set(true);
browser.setMediaItems(MappingUtil.mapMediaItems(media)); browser.setMediaItems(MappingUtil.mapMediaItems(media), startIndex, 0);
browser.prepare(); browser.prepare();
Player.Listener timelineListener = new Player.Listener() { Player.Listener timelineListener = new Player.Listener() {
@ -210,10 +212,11 @@ public class MediaManager {
mediaBrowserListenableFuture.addListener(() -> { mediaBrowserListenableFuture.addListener(() -> {
try { try {
if (mediaBrowserListenableFuture.isDone()) { if (mediaBrowserListenableFuture.isDone()) {
mediaBrowserListenableFuture.get().clearMediaItems(); MediaBrowser browser = mediaBrowserListenableFuture.get();
mediaBrowserListenableFuture.get().setMediaItem(MappingUtil.mapMediaItem(media)); justStarted.set(true);
mediaBrowserListenableFuture.get().prepare(); browser.setMediaItem(MappingUtil.mapMediaItem(media));
mediaBrowserListenableFuture.get().play(); browser.prepare();
browser.play();
enqueueDatabase(media, true, 0); enqueueDatabase(media, true, 0);
} }
} catch (ExecutionException | InterruptedException e) { } catch (ExecutionException | InterruptedException e) {
@ -229,7 +232,7 @@ public class MediaManager {
try { try {
if (mediaBrowserListenableFuture.isDone()) { if (mediaBrowserListenableFuture.isDone()) {
MediaBrowser mediaBrowser = mediaBrowserListenableFuture.get(); MediaBrowser mediaBrowser = mediaBrowserListenableFuture.get();
mediaBrowser.clearMediaItems(); justStarted.set(true);
mediaBrowser.setMediaItem(mediaItem); mediaBrowser.setMediaItem(mediaItem);
mediaBrowser.prepare(); mediaBrowser.prepare();
mediaBrowser.play(); mediaBrowser.play();
@ -247,10 +250,11 @@ public class MediaManager {
mediaBrowserListenableFuture.addListener(() -> { mediaBrowserListenableFuture.addListener(() -> {
try { try {
if (mediaBrowserListenableFuture.isDone()) { if (mediaBrowserListenableFuture.isDone()) {
mediaBrowserListenableFuture.get().clearMediaItems(); MediaBrowser browser = mediaBrowserListenableFuture.get();
mediaBrowserListenableFuture.get().setMediaItem(MappingUtil.mapInternetRadioStation(internetRadioStation)); justStarted.set(true);
mediaBrowserListenableFuture.get().prepare(); browser.setMediaItem(MappingUtil.mapInternetRadioStation(internetRadioStation));
mediaBrowserListenableFuture.get().play(); browser.prepare();
browser.play();
} }
} catch (ExecutionException | InterruptedException e) { } catch (ExecutionException | InterruptedException e) {
e.printStackTrace(); e.printStackTrace();
@ -264,10 +268,11 @@ public class MediaManager {
mediaBrowserListenableFuture.addListener(() -> { mediaBrowserListenableFuture.addListener(() -> {
try { try {
if (mediaBrowserListenableFuture.isDone()) { if (mediaBrowserListenableFuture.isDone()) {
mediaBrowserListenableFuture.get().clearMediaItems(); MediaBrowser browser = mediaBrowserListenableFuture.get();
mediaBrowserListenableFuture.get().setMediaItem(MappingUtil.mapMediaItem(podcastEpisode)); justStarted.set(true);
mediaBrowserListenableFuture.get().prepare(); browser.setMediaItem(MappingUtil.mapMediaItem(podcastEpisode));
mediaBrowserListenableFuture.get().play(); browser.prepare();
browser.play();
} }
} catch (ExecutionException | InterruptedException e) { } catch (ExecutionException | InterruptedException e) {
e.printStackTrace(); e.printStackTrace();
@ -281,9 +286,11 @@ public class MediaManager {
mediaBrowserListenableFuture.addListener(() -> { mediaBrowserListenableFuture.addListener(() -> {
try { try {
if (mediaBrowserListenableFuture.isDone()) { if (mediaBrowserListenableFuture.isDone()) {
if (playImmediatelyAfter && mediaBrowserListenableFuture.get().getNextMediaItemIndex() != -1) { Log.e(TAG, "enqueue");
enqueueDatabase(media, false, mediaBrowserListenableFuture.get().getNextMediaItemIndex()); MediaBrowser browser = mediaBrowserListenableFuture.get();
mediaBrowserListenableFuture.get().addMediaItems(mediaBrowserListenableFuture.get().getNextMediaItemIndex(), MappingUtil.mapMediaItems(media)); if (playImmediatelyAfter && browser.getNextMediaItemIndex() != -1) {
enqueueDatabase(media, false, browser.getNextMediaItemIndex());
browser.addMediaItems(browser.getNextMediaItemIndex(), MappingUtil.mapMediaItems(media));
} else { } else {
enqueueDatabase(media, false, mediaBrowserListenableFuture.get().getMediaItemCount()); enqueueDatabase(media, false, mediaBrowserListenableFuture.get().getMediaItemCount());
mediaBrowserListenableFuture.get().addMediaItems(MappingUtil.mapMediaItems(media)); mediaBrowserListenableFuture.get().addMediaItems(MappingUtil.mapMediaItems(media));
@ -301,9 +308,11 @@ public class MediaManager {
mediaBrowserListenableFuture.addListener(() -> { mediaBrowserListenableFuture.addListener(() -> {
try { try {
if (mediaBrowserListenableFuture.isDone()) { if (mediaBrowserListenableFuture.isDone()) {
if (playImmediatelyAfter && mediaBrowserListenableFuture.get().getNextMediaItemIndex() != -1) { Log.e(TAG, "enqueue");
enqueueDatabase(media, false, mediaBrowserListenableFuture.get().getNextMediaItemIndex()); MediaBrowser browser = mediaBrowserListenableFuture.get();
mediaBrowserListenableFuture.get().addMediaItem(mediaBrowserListenableFuture.get().getNextMediaItemIndex(), MappingUtil.mapMediaItem(media)); if (playImmediatelyAfter && browser.getNextMediaItemIndex() != -1) {
enqueueDatabase(media, false, browser.getNextMediaItemIndex());
browser.addMediaItem(browser.getNextMediaItemIndex(), MappingUtil.mapMediaItem(media));
} else { } else {
enqueueDatabase(media, false, mediaBrowserListenableFuture.get().getMediaItemCount()); enqueueDatabase(media, false, mediaBrowserListenableFuture.get().getMediaItemCount());
mediaBrowserListenableFuture.get().addMediaItem(MappingUtil.mapMediaItem(media)); mediaBrowserListenableFuture.get().addMediaItem(MappingUtil.mapMediaItem(media));
@ -321,8 +330,10 @@ public class MediaManager {
mediaBrowserListenableFuture.addListener(() -> { mediaBrowserListenableFuture.addListener(() -> {
try { try {
if (mediaBrowserListenableFuture.isDone()) { if (mediaBrowserListenableFuture.isDone()) {
mediaBrowserListenableFuture.get().removeMediaItems(startIndex, endIndex + 1); Log.e(TAG, "shuffle");
mediaBrowserListenableFuture.get().addMediaItems(MappingUtil.mapMediaItems(media).subList(startIndex, endIndex + 1)); MediaBrowser browser = mediaBrowserListenableFuture.get();
browser.removeMediaItems(startIndex, endIndex + 1);
browser.addMediaItems(MappingUtil.mapMediaItems(media).subList(startIndex, endIndex + 1));
swapDatabase(media); swapDatabase(media);
} }
} catch (ExecutionException | InterruptedException e) { } catch (ExecutionException | InterruptedException e) {
@ -337,6 +348,7 @@ public class MediaManager {
mediaBrowserListenableFuture.addListener(() -> { mediaBrowserListenableFuture.addListener(() -> {
try { try {
if (mediaBrowserListenableFuture.isDone()) { if (mediaBrowserListenableFuture.isDone()) {
Log.e(TAG, "swap");
mediaBrowserListenableFuture.get().moveMediaItem(from, to); mediaBrowserListenableFuture.get().moveMediaItem(from, to);
swapDatabase(media); swapDatabase(media);
} }
@ -352,6 +364,7 @@ public class MediaManager {
mediaBrowserListenableFuture.addListener(() -> { mediaBrowserListenableFuture.addListener(() -> {
try { try {
if (mediaBrowserListenableFuture.isDone()) { if (mediaBrowserListenableFuture.isDone()) {
Log.e(TAG, "remove");
if (mediaBrowserListenableFuture.get().getMediaItemCount() > 1 && mediaBrowserListenableFuture.get().getCurrentMediaItemIndex() != toRemove) { if (mediaBrowserListenableFuture.get().getMediaItemCount() > 1 && mediaBrowserListenableFuture.get().getCurrentMediaItemIndex() != toRemove) {
mediaBrowserListenableFuture.get().removeMediaItem(toRemove); mediaBrowserListenableFuture.get().removeMediaItem(toRemove);
removeDatabase(media, toRemove); removeDatabase(media, toRemove);
@ -371,6 +384,7 @@ public class MediaManager {
mediaBrowserListenableFuture.addListener(() -> { mediaBrowserListenableFuture.addListener(() -> {
try { try {
if (mediaBrowserListenableFuture.isDone()) { if (mediaBrowserListenableFuture.isDone()) {
Log.e(TAG, "remove range");
mediaBrowserListenableFuture.get().removeMediaItems(fromItem, toItem); mediaBrowserListenableFuture.get().removeMediaItems(fromItem, toItem);
removeRangeDatabase(media, fromItem, toItem); removeRangeDatabase(media, fromItem, toItem);
} }
@ -420,6 +434,7 @@ public class MediaManager {
@Override @Override
public void onChanged(List<Child> media) { public void onChanged(List<Child> media) {
if (media != null) { if (media != null) {
Log.e(TAG, "continuous play");
ListenableFuture<MediaBrowser> mediaBrowserListenableFuture = new MediaBrowser.Builder( ListenableFuture<MediaBrowser> mediaBrowserListenableFuture = new MediaBrowser.Builder(
App.getContext(), App.getContext(),
new SessionToken(App.getContext(), new ComponentName(App.getContext(), MediaService.class)) new SessionToken(App.getContext(), new ComponentName(App.getContext(), MediaService.class))

View file

@ -19,6 +19,7 @@ import androidx.fragment.app.Fragment
import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.UnstableApi
import com.cappielloantonio.tempo.R import com.cappielloantonio.tempo.R
import com.cappielloantonio.tempo.service.EqualizerManager import com.cappielloantonio.tempo.service.EqualizerManager
import com.cappielloantonio.tempo.service.BaseMediaService
import com.cappielloantonio.tempo.service.MediaService import com.cappielloantonio.tempo.service.MediaService
import com.cappielloantonio.tempo.util.Preferences import com.cappielloantonio.tempo.util.Preferences
@ -35,7 +36,7 @@ class EqualizerFragment : Fragment() {
private val equalizerUpdatedReceiver = object : BroadcastReceiver() { private val equalizerUpdatedReceiver = object : BroadcastReceiver() {
@OptIn(UnstableApi::class) @OptIn(UnstableApi::class)
override fun onReceive(context: Context?, intent: Intent?) { override fun onReceive(context: Context?, intent: Intent?) {
if (intent?.action == MediaService.ACTION_EQUALIZER_UPDATED) { if (intent?.action == BaseMediaService.ACTION_EQUALIZER_UPDATED) {
initUI() initUI()
restoreEqualizerPreferences() restoreEqualizerPreferences()
} }
@ -45,7 +46,7 @@ class EqualizerFragment : Fragment() {
private val connection = object : ServiceConnection { private val connection = object : ServiceConnection {
@OptIn(UnstableApi::class) @OptIn(UnstableApi::class)
override fun onServiceConnected(className: ComponentName, service: IBinder) { override fun onServiceConnected(className: ComponentName, service: IBinder) {
val binder = service as MediaService.LocalBinder val binder = service as BaseMediaService.LocalBinder
equalizerManager = binder.getEqualizerManager() equalizerManager = binder.getEqualizerManager()
initUI() initUI()
restoreEqualizerPreferences() restoreEqualizerPreferences()
@ -60,14 +61,14 @@ class EqualizerFragment : Fragment() {
override fun onStart() { override fun onStart() {
super.onStart() super.onStart()
Intent(requireContext(), MediaService::class.java).also { intent -> 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) requireActivity().bindService(intent, connection, Context.BIND_AUTO_CREATE)
} }
if (!receiverRegistered) { if (!receiverRegistered) {
ContextCompat.registerReceiver( ContextCompat.registerReceiver(
requireContext(), requireContext(),
equalizerUpdatedReceiver, equalizerUpdatedReceiver,
IntentFilter(MediaService.ACTION_EQUALIZER_UPDATED), IntentFilter(BaseMediaService.ACTION_EQUALIZER_UPDATED),
ContextCompat.RECEIVER_NOT_EXPORTED ContextCompat.RECEIVER_NOT_EXPORTED
) )
receiverRegistered = true receiverRegistered = true

View file

@ -1,11 +1,12 @@
package com.cappielloantonio.tempo.util; package com.cappielloantonio.tempo.util;
import androidx.annotation.OptIn; import androidx.annotation.OptIn;
import androidx.media3.common.C;
import androidx.media3.common.MediaItem; import androidx.media3.common.MediaItem;
import androidx.media3.common.Metadata; import androidx.media3.common.Metadata;
import androidx.media3.common.Tracks; import androidx.media3.common.Tracks;
import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.UnstableApi;
import androidx.media3.exoplayer.ExoPlayer; import androidx.media3.common.Player;
import com.cappielloantonio.tempo.model.ReplayGain; import com.cappielloantonio.tempo.model.ReplayGain;
@ -17,7 +18,7 @@ import java.util.Objects;
public class ReplayGainUtil { public class ReplayGainUtil {
private static final String[] tags = {"REPLAYGAIN_TRACK_GAIN", "REPLAYGAIN_ALBUM_GAIN", "R128_TRACK_GAIN", "R128_ALBUM_GAIN"}; 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<Metadata> metadata = getMetadata(tracks);
List<ReplayGain> gains = getReplayGains(metadata); 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()); if (gains.size() == 1) gains.add(1, new ReplayGain());
return gains; 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()) { if (Objects.equals(Preferences.getReplayGainMode(), "disabled") || gains == null || gains.isEmpty()) {
setNoReplayGain(player); setNoReplayGain(player);
return; return;
@ -137,33 +138,33 @@ public class ReplayGainUtil {
setNoReplayGain(player); setNoReplayGain(player);
} }
private static void setNoReplayGain(ExoPlayer player) { private static void setNoReplayGain(Player player) {
setReplayGain(player, 0f); 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(); float trackGain = gains.get(0).getTrackGain() != 0f ? gains.get(0).getTrackGain() : gains.get(1).getTrackGain();
setReplayGain(player, trackGain != 0f ? trackGain : 0f); 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(); float albumGain = gains.get(0).getAlbumGain() != 0f ? gains.get(0).getAlbumGain() : gains.get(1).getAlbumGain();
setReplayGain(player, albumGain != 0f ? albumGain : 0f); 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 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(); float trackGain = gains.get(0).getTrackGain() != 0f ? gains.get(0).getTrackGain() : gains.get(1).getTrackGain();
setReplayGain(player, albumGain != 0f ? albumGain : trackGain); setReplayGain(player, albumGain != 0f ? albumGain : trackGain);
} }
private static boolean areTracksConsecutive(ExoPlayer player) { private static boolean areTracksConsecutive(Player player) {
MediaItem currentMediaItem = player.getCurrentMediaItem(); MediaItem currentMediaItem = player.getCurrentMediaItem();
int currentMediaItemIndex = player.getCurrentMediaItemIndex(); int prevMediaItemIndex = player.getPreviousMediaItemIndex();
MediaItem pastMediaItem = currentMediaItemIndex > 0 ? player.getMediaItemAt(currentMediaItemIndex - 1) : null; MediaItem pastMediaItem = prevMediaItemIndex == C.INDEX_UNSET ? null : player.getMediaItemAt(prevMediaItemIndex);
return currentMediaItem != null && return currentMediaItem != null &&
pastMediaItem != null && pastMediaItem != null &&
@ -172,7 +173,7 @@ public class ReplayGainUtil {
pastMediaItem.mediaMetadata.albumTitle.toString().equals(currentMediaItem.mediaMetadata.albumTitle.toString()); 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)); player.setVolume((float) Math.pow(10f, gain / 20f));
} }
} }

View file

@ -177,6 +177,7 @@
<string name="menu_filter_download">Descargado</string> <string name="menu_filter_download">Descargado</string>
<string name="menu_group_by_album">Álbum</string> <string name="menu_group_by_album">Álbum</string>
<string name="menu_group_by_artist">Artista</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_scan_result">Escaneo: hay %1$d pistas</string>
<string name="settings_support_title">Soporte al usuario</string> <string name="settings_support_title">Soporte al usuario</string>
<string name="settings_image_size">Resolución de la imagen</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_logout_title">Cerrar sesión</string>
<string name="settings_github_link">https://github.com/eddyizm/tempus</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_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_genre">Género</string>
<string name="menu_group_by_track">Pista</string> <string name="menu_group_by_track">Pista</string>
<string name="menu_group_by_year">Año</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_artist">Artista</string>
<string name="menu_sort_name">Nombre</string> <string name="menu_sort_name">Nombre</string>
<string name="menu_sort_random">Aleatorio</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_added">Añadido recientemente</string>
<string name="menu_sort_recently_played">Reproducido recientemente</string> <string name="menu_sort_recently_played">Reproducido recientemente</string>
<string name="menu_sort_most_played">Lo más reproducido</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_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="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_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_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_show_mini_shuffle_button">Mostrar el botón «Aleatorio»</string>
<string name="settings_auto_download_lyrics">Descargar automáticamente las letras</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_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_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="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> </resources>

View file

@ -254,4 +254,4 @@
<item>3</item> <item>3</item>
<item>4</item> <item>4</item>
</string-array> </string-array>
</resources> </resources>

View file

@ -1,417 +1,536 @@
<resources> <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_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_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_battery_optimizations_title">Ottimizzazioni della Batteria</string>
<string name="activity_info_offline_mode">Modalità offline</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_playlist">Aggiungi alla playlist</string>
<string name="album_bottom_sheet_add_to_queue">Aggiungi alla coda</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_download_all">Scarica tutto</string>
<string name="album_bottom_sheet_go_to_artist">Vai all\'artista</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_instant_mix">Mix istantaneo</string>
<string name="album_bottom_sheet_play_next">Riproduci successivo</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_remove_all">Rimuovi tutto</string>
<string name="album_bottom_sheet_share">Condividi</string> <string name="album_bottom_sheet_share">Condividi</string>
<string name="album_bottom_sheet_shuffle">Riproduzione casuale</string> <string name="album_bottom_sheet_shuffle">Riproduzione casuale</string>
<string name="album_catalogue_title">Album</string> <string name="album_catalogue_title">Album</string>
<string name="album_catalogue_title_expanded">Sfoglia 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_error_retrieving_artist">Errore nel recupero dell\'artista</string>
<string name="album_list_page_downloaded">Album scaricati</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_most_played">Album più riprodotti</string>
<string name="album_list_page_new_releases">Nuove uscite</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_added">Album aggiunti di recente</string>
<string name="album_list_page_recently_played">Album riprodotti 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_starred">Album preferiti</string>
<string name="album_list_page_title">Album</string> <string name="album_list_page_title">Album</string>
<string name="album_page_extra_info_button">Simili a questo</string> <string name="album_page_extra_info_button">Altri simili</string>
<string name="album_page_play_button">Riproduci</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_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_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_shuffle_button">Riproduzione casuale</string>
<string name="album_page_tracks_count_and_duration">%1$d brani • %2$d minuti</string> <string name="album_page_tracks_count_and_duration">%1$d brani • %2$d minuti</string>
<string name="app_name">Tempus</string> <string name="app_name">Tempus</string>
<string name="artist_adapter_radio_station_starting">Ricerca in corso…</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_instant_mix">Mix istantaneo</string>
<string name="artist_bottom_sheet_shuffle">Riproduzione casuale</string> <string name="artist_bottom_sheet_shuffle">Riproduzione casuale</string>
<string name="artist_catalogue_title">Artisti</string> <string name="artist_catalogue_title">Artisti</string>
<string name="artist_catalogue_title_expanded">Sfoglia 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_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_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_downloaded">Artisti scaricati</string>
<string name="artist_list_page_starred">Artisti preferiti</string> <string name="artist_list_page_starred">Artisti preferiti</string>
<string name="artist_list_page_title">Artisti</string> <string name="artist_list_page_title">Artisti</string>
<string name="artist_page_radio_button">Radio</string> <string name="artist_page_radio_button">Radio</string>
<string name="artist_page_shuffle_button">Riproduzione casuale</string> <string name="artist_page_shuffle_button">Riproduzione casuale</string>
<string name="artist_page_switch_layout_button">Cambia layout</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_more_like_this_button">Altri simili</string>
<string name="artist_page_title_album_section">Album</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_more_button">Altro</string>
<string name="artist_page_title_biography_section">Biografia</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_section">Brani più ascoltati</string>
<string name="artist_page_title_most_streamed_song_see_all_button">Vedi tutto</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_negative_button">Ignora</string>
<string name="battery_optimization_neutral_button">Non chiedere di nuovo</string> <string name="battery_optimization_neutral_button">Non chiedere di nuovo</string>
<string name="battery_optimization_positive_button">Disabilita</string> <string name="battery_optimization_positive_button">Disabilita</string>
<string name="connection_alert_dialog_negative_button">Annulla</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_neutral_button">Attiva risparmio dati</string>
<string name="connection_alert_dialog_positive_button">OK</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_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="connection_alert_dialog_title">Wi-Fi non connesso</string>
<string name="content_description_shuffle_button">Riproduzione casuale</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_negative_button">Annulla</string>
<string name="delete_download_storage_dialog_positive_button">Continua</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_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="delete_download_storage_dialog_title">Elimina elementi salvati</string>
<string name="description_empty_title">Descrizione non disponibile</string> <string name="description_empty_title">Descrizione non disponibile</string>
<string name="disc_titlefull">Disco %1$s - %2$s</string> <string name="disc_titlefull">Disco %1$s - %2$s</string>
<string name="disc_titleless">Disco %1$s</string> <string name="disc_titleless">Disco %1$s</string>
<string name="download_directory_dialog_negative_button">Annulla</string> <string name="download_directory_dialog_negative_button">Annulla</string>
<string name="download_directory_dialog_positive_button">Scarica</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_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_dialog_title">Scarica i brani</string>
<string name="download_info_empty_subtitle">Una volta scaricato un brano, lo troverai qui</string> <string name="download_directory_set">Imposta dove scaricare la musica</string>
<string name="download_info_empty_title">Nessun download ancora!</string> <string name="download_info_empty_subtitle">Una volta scaricato un brano, lo troverai qui</string>
<string name="download_item_multiple_subtitle_formatter">%1$s • %2$s elementi</string> <string name="download_info_empty_title">Ancora nessun download!</string>
<string name="download_item_single_subtitle_formatter">%1$s elementi</string> <string name="download_item_multiple_subtitle_formatter">%1$s • %2$s elementi</string>
<string name="download_shuffle_all_subtitle">Riproduzione casuale di tutto</string> <string name="download_item_single_subtitle_formatter">%1$s elementi</string>
<string name="download_storage_dialog_sub_summary">Per rendere effettive le modifiche, riavvia l\'app.</string> <string name="download_shuffle_all_subtitle">Riproduzione casuale di tutto</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_sub_summary">Per rendere effettive le modifiche, riavvia l\'app.</string>
<string name="download_storage_dialog_title">Seleziona opzione di memoria</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_external_dialog_positive_button">Esterna</string> <string name="download_storage_dialog_title">Seleziona opzione di memoria</string>
<string name="download_storage_internal_dialog_negative_button">Interna</string> <string name="download_storage_external_dialog_positive_button">Esterna</string>
<string name="download_title_section">Download</string> <string name="download_storage_internal_dialog_negative_button">Interna</string>
<string name="downloaded_bottom_sheet_add_to_queue">Aggiungi alla coda</string> <string name="download_storage_directory_dialog_neutral_button">Cartella</string>
<string name="downloaded_bottom_sheet_play_next">Riproduci successivo</string> <string name="download_title_section">Scarica</string>
<string name="downloaded_bottom_sheet_remove">Rimuovi</string> <string name="download_refresh_no_directory">Imposta una cartella di download per aggiornare i tuoi download.</string>
<string name="downloaded_bottom_sheet_remove_all">Rimuovi tutto</string> <string name="download_refresh_no_changes">Nessun download mancante trovato.</string>
<string name="downloaded_bottom_sheet_shuffle">Riproduzione casuale</string> <plurals name="download_refresh_removed">
<string name="empty_string" /> <item quantity="one">Rimosso %d download mancante.</item>
<string name="error_required">Obbligatorio</string> <item quantity="other">Rimossi %d download mancanti.</item>
<string name="error_server_prefix">Prefisso http o https richiesto</string> </plurals>
<string name="exo_download_notification_channel_name">Download</string> <string name="download_refresh_button_content_description">Aggiorna gli elementi scaricati</string>
<string name="filter_info_selection">Seleziona due o più filtri</string> <string name="downloaded_bottom_sheet_add_to_queue">Aggiungi alla coda</string>
<string name="filter_title">Filtro</string> <string name="downloaded_bottom_sheet_play_next">Riproduci dopo</string>
<string name="filter_title_expanded">Filtra Generi</string> <string name="downloaded_bottom_sheet_remove">Rimuovi</string>
<string name="genre_catalogue_title">Catalogo dei Generi</string> <string name="downloaded_bottom_sheet_remove_all">Rimuovi tutto</string>
<string name="genre_catalogue_title_expanded">Sfoglia Generi</string> <string name="downloaded_bottom_sheet_shuffle">Riproduzione casuale</string>
<string name="github_update_dialog_negative_button">Ricordamelo più tardi</string> <string name="empty_string" />
<string name="github_update_dialog_neutral_button">Supportami</string> <string name="error_required">Obbligatorio</string>
<string name="github_update_dialog_positive_button">Scarica ora</string> <string name="error_server_prefix">Prefisso http o https richiesto</string>
<string name="github_update_dialog_summary">È disponibile una nuova versione dell\'app su Github.</string> <string name="exo_download_notification_channel_name">Download</string>
<string name="github_update_dialog_title">Aggiornamento disponibile</string> <string name="exo_controls_heart_off_description">Aggiungi ai preferiti</string>
<string name="home_rearrangement_dialog_negative_button">Annulla</string> <string name="exo_controls_heart_on_description">Rimuovi dai preferiti</string>
<string name="home_rearrangement_dialog_neutral_button">Reimposta</string> <string name="cast_expanded_controller_loading">Caricamento…</string>
<string name="home_rearrangement_dialog_positive_button">Salva</string> <string name="filter_info_selection">Seleziona due o più filtri</string>
<string name="home_rearrangement_dialog_title">Riorganizza home</string> <string name="filter_title">Filtro</string>
<string name="home_rearrangement_dialog_subtitle">Si prega di notare che per rendere effettive le modifiche è necessario riavviare l\'applicazione.</string> <string name="filter_artist">Filtra artisti</string>
<string name="home_subtitle_best_of">Le migliori canzoni dei tuoi artisti preferiti</string> <string name="filter_title_expanded">Filtra Generi</string>
<string name="home_subtitle_made_for_you">Inizia un mix da una canzone che ti è piaciuta</string> <string name="generic_list_page_count">(%1$d)</string>
<string name="home_subtitle_new_internet_radio_station">Aggiungi una nuova radio</string> <string name="generic_list_page_count_unknown">(+%1$d)</string>
<string name="home_subtitle_new_podcast_channel">Aggiungi un nuovo canale podcast</string> <string name="genre_catalogue_title">Catalogo dei Generi</string>
<string name="home_sync_starred_cancel">Annulla</string> <string name="genre_catalogue_title_expanded">Sfoglia Generi</string>
<string name="home_sync_starred_download">Scarica</string> <string name="github_update_dialog_negative_button">Ricordamelo più tardi</string>
<string name="home_sync_starred_subtitle">Scaricare questi brani potrebbe comportare un uso significativo di dati</string> <string name="github_update_dialog_neutral_button">Supportami</string>
<string name="home_sync_starred_title">Sembra che ci siano brani da sincronizzare con una stella</string> <string name="github_update_dialog_positive_button">Scarica ora</string>
<string name="home_title_best_of">Il meglio di</string> <string name="github_update_dialog_summary">È disponibile una nuova versione dell\'app su Github.</string>
<string name="home_title_discovery">Scoperta</string> <string name="github_update_dialog_title">Aggiornamento disponibile</string>
<string name="home_title_discovery_shuffle_all_button">Mescola tutto</string> <string name="home_rearrangement_dialog_negative_button">Annulla</string>
<string name="home_title_flashback">Flashback</string> <string name="home_rearrangement_dialog_neutral_button">Ripristina</string>
<string name="home_title_internet_radio_station">Stazioni radio internet</string> <string name="home_rearrangement_dialog_positive_button">Salva</string>
<string name="home_title_last_played">Ultimi ascolti</string> <string name="home_rearrangement_dialog_title">Riorganizza home</string>
<string name="home_title_last_played_see_all_button">Vedi tutto</string> <string name="home_rearrangement_dialog_subtitle">Per rendere effettive le modifiche è necessario riavviare l\'applicazione.</string>
<string name="home_title_last_week">La scorsa settimana</string> <string name="home_section_music">Musica</string>
<string name="home_title_last_month">Il mese scorso</string> <string name="home_section_podcast">Podcast</string>
<string name="home_title_last_year">L\'anno scorso</string> <string name="home_section_radio">Radio</string>
<string name="home_title_made_for_you">Fatto per te</string> <string name="home_subtitle_best_of">Le migliori canzoni dei tuoi artisti preferiti</string>
<string name="home_title_most_played">Più ascoltati</string> <string name="home_subtitle_made_for_you">Inizia un mix da una canzone che ti è piaciuta</string>
<string name="home_title_most_played_see_all_button">Vedi tutto</string> <string name="home_subtitle_new_internet_radio_station">Aggiungi una nuova radio</string>
<string name="home_title_new_releases">Nuove uscite</string> <string name="home_subtitle_new_podcast_channel">Aggiungi un nuovo canale podcast</string>
<string name="home_title_newest_podcasts">Podcast più recenti</string> <string name="home_sync_starred_cancel">Annulla</string>
<string name="home_title_pinned_playlists">Playlist</string> <string name="home_sync_starred_download">Scarica</string>
<string name="home_title_podcast_channels">Canali</string> <string name="home_sync_starred_subtitle">Scaricare questi brani potrebbe comportare un uso significativo di dati</string>
<string name="home_title_podcast_channels_see_all_button">Vedi tutto</string> <string name="home_sync_starred_title">Sembra che ci siano alcuni brani preferiti da sincronizzare</string>
<string name="home_title_radio_station">Stazioni radio</string> <string name="home_sync_starred_albums_title">Sincronizza Album Preferiti</string>
<string name="home_title_recently_added">Aggiunti di recente</string> <string name="home_sync_starred_albums_subtitle">Gli album preferiti saranno disponibili offline</string>
<string name="home_title_recently_added_see_all_button">Vedi tutto</string> <string name="home_sync_starred_artists_title">Sincronizza Artisti Preferiti</string>
<string name="home_title_shares">Condivisioni</string> <string name="home_sync_starred_artists_subtitle">Hai artisti preferiti con musica non scaricata</string>
<string name="home_title_starred_albums">★ Album con stella</string> <string name="home_title_best_of">Il meglio di</string>
<string name="home_title_starred_albums_see_all_button">Vedi tutto</string> <string name="home_title_discovery">Scopri</string>
<string name="home_title_starred_artists">★ Artisti con stella</string> <string name="home_title_discovery_shuffle_all_button">Mescola tutto</string>
<string name="home_title_starred_artists_see_all_button">Vedi tutto</string> <string name="home_title_flashback">Flashback</string>
<string name="home_title_starred_tracks">★ Brani con stella</string> <string name="home_title_internet_radio_station">Stazioni internet-radio</string>
<string name="home_title_starred_tracks_see_all_button">Vedi tutto</string> <string name="home_title_last_played">Ultimi ascolti</string>
<string name="home_title_top_songs">I tuoi migliori brani</string> <string name="home_title_last_played_see_all_button">Vedi tutto</string>
<string name="home_option_reorganize">Riorganizza</string> <string name="home_title_last_week">La scorsa settimana</string>
<string name="label_dot_separator" translatable="false"></string> <string name="home_title_last_month">Il mese scorso</string>
<string name="label_placeholder" translatable="false">--</string> <string name="home_title_last_year">L\'anno scorso</string>
<string name="library_title_album">Album</string> <string name="home_title_made_for_you">Fatto per te</string>
<string name="library_title_album_see_all_button">Vedi tutto</string> <string name="home_title_most_played">Più ascoltati</string>
<string name="library_title_artist">Artisti</string> <string name="home_title_most_played_see_all_button">Vedi tutto</string>
<string name="library_title_artist_see_all_button">Vedi tutto</string> <string name="home_title_new_releases">Nuove uscite</string>
<string name="library_title_genre">Generi</string> <string name="home_title_newest_podcasts">Podcast più recenti</string>
<string name="library_title_genre_see_all_button">Vedi tutto</string> <string name="home_title_pinned_playlists">Playlist</string>
<string name="library_title_music_folder">Cartelle musicali</string> <string name="home_title_podcast_channels">Canali</string>
<string name="library_title_playlist">Playlist</string> <string name="home_title_podcast_channels_see_all_button">Vedi tutto</string>
<string name="library_title_playlist_see_all_button">Vedi tutto</string> <string name="home_title_radio_station">Stazioni radio</string>
<string name="login_empty">Nessun server aggiunto</string> <string name="home_title_recently_added">Aggiunti di recente</string>
<string name="login_title">Server Subsonic</string> <string name="home_title_recently_added_see_all_button">Vedi tutto</string>
<string name="login_title_expanded">Server Subsonic</string> <string name="home_title_shares">Condivisioni</string>
<string name="media_route_menu_title">Trasmetti</string> <string name="home_title_starred_albums">★ Album preferiti</string>
<string name="menu_add_button">Aggiungi</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_add_to_playlist_button">Aggiungi alla playlist</string>
<string name="menu_download_all_button">Scarica tutto</string> <string name="menu_download_all_button">Scarica tutto</string>
<string name="menu_download_label">Scarica</string> <string name="menu_rate_album">Valuta l\'album</string>
<string name="menu_filter_all">Tutti</string> <string name="menu_download_label">Scarica</string>
<string name="menu_filter_download">Scaricati</string> <string name="menu_filter_all">Tutti</string>
<string name="menu_group_by_album">Album</string> <string name="menu_filter_download">Scaricati</string>
<string name="menu_group_by_artist">Artista</string> <string name="menu_group_by_album">Album</string>
<string name="menu_group_by_genre">Genere</string> <string name="menu_group_by_artist">Artista</string>
<string name="menu_group_by_track">Brano</string> <string name="menu_group_by_genre">Genere</string>
<string name="menu_group_by_year">Anno</string> <string name="menu_group_by_track">Brano</string>
<string name="menu_home_label">Home</string> <string name="menu_group_by_year">Anno</string>
<string name="menu_last_week_name">La scorsa settimana</string> <string name="menu_home_label">Home</string>
<string name="menu_last_month_name">Il mese scorso</string> <string name="menu_last_week_name">La scorsa settimana</string>
<string name="menu_last_year_name">L\'anno scorso</string> <string name="menu_last_month_name">Il mese scorso</string>
<string name="menu_library_label">Libreria</string> <string name="menu_last_year_name">L\'anno scorso</string>
<string name="menu_search_button">Cerca</string> <string name="menu_library_label">Libreria</string>
<string name="menu_settings_button">Impostazioni</string> <string name="menu_search_button">Cerca</string>
<string name="menu_sort_artist">Artista</string> <string name="menu_settings_button">Impostazioni</string>
<string name="menu_sort_name">Nome</string> <string name="menu_sort_artist">Artista</string>
<string name="menu_sort_random">Casuale</string> <string name="menu_sort_name">Nome</string>
<string name="menu_sort_recently_added">Aggiunti di recente</string> <string name="menu_sort_random">Casuale</string>
<string name="menu_pin_button">Aggiungi alla schermata home</string> <string name="menu_sort_album_count">Numero di Album</string>
<string name="menu_unpin_button">Rimuovi dalla schermata home</string> <string name="menu_sort_recently_added">Aggiunti di recente</string>
<string name="menu_sort_year">Anno</string> <string name="menu_sort_recently_played">Riprodotti di recente</string>
<string name="player_playback_speed">%1$.2fx</string> <string name="menu_sort_most_played">Più riprodotti</string>
<string name="player_queue_clean_all_button">Svuota coda di riproduzione</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_queue_save_queue_success">Salvato</string>
<string name="player_server_priority">Priorità server</string> <string name="player_lyrics_download_content_description">Scarica i testi delle canzoni per riprodurli offline</string>
<string name="playlist_catalogue_title">Catalogo playlist</string> <string name="player_lyrics_downloaded_content_description">Testi scaricati per la riproduzione offline</string>
<string name="playlist_catalogue_title_expanded">Sfoglia le playlist</string> <string name="player_lyrics_download_success">Testi salvati per la riproduzione offline.</string>
<string name="playlist_chooser_dialog_empty">Nessuna playlist creata</string> <string name="player_lyrics_download_failure">I testi non sono disponibili per il download.</string>
<string name="playlist_chooser_dialog_negative_button">Annulla</string> <string name="player_server_priority">Priorità server</string>
<string name="playlist_chooser_dialog_neutral_button">Crea</string> <string name="player_unknown_format">Formato sconosciuto</string>
<string name="playlist_chooser_dialog_title">Aggiungi a una playlist</string> <string name="player_transcoding">Transcodifica</string>
<string name="playlist_chooser_dialog_toast_add_success">Aggiunta di un brano alla playlist</string> <string name="player_transcoding_requested">richiesto</string>
<string name="playlist_chooser_dialog_toast_add_failure">Impossibile aggiungere un brano alla playlist</string> <string name="playlist_catalogue_title">Catalogo playlist</string>
<string name="playlist_counted_tracks">%1$d brani • %2$s</string> <string name="playlist_catalogue_title_expanded">Sfoglia le playlist</string>
<string name="playlist_duration">Durata • %1$s</string> <string name="playlist_chooser_dialog_empty">Nessuna playlist creata</string>
<string name="playlist_editor_dialog_action_delete_toast">Premi a lungo per eliminare</string> <string name="playlist_chooser_dialog_negative_button">Annulla</string>
<string name="playlist_editor_dialog_hint_name">Nome della playlist</string> <string name="playlist_chooser_dialog_neutral_button">Crea</string>
<string name="playlist_editor_dialog_negative_button">Annulla</string> <string name="playlist_chooser_dialog_title">Aggiungi a una playlist</string>
<string name="playlist_editor_dialog_neutral_button">Elimina</string> <string name="playlist_chooser_dialog_toast_add_success">Aggiunta di un brano alla playlist</string>
<string name="playlist_editor_dialog_positive_button">Salva</string> <string name="playlist_chooser_dialog_toast_add_failure">Impossibile aggiungere un brano alla playlist</string>
<string name="playlist_editor_dialog_title">Modifica playlist</string> <string name="playlist_chooser_dialog_toast_all_skipped">Tutte le canzoni sono state saltate perché duplicate</string>
<string name="playlist_page_play_button">Riproduci</string> <string name="playlist_counted_tracks">%1$d brani • %2$s</string>
<string name="playlist_page_shuffle_button">Mescola</string> <string name="playlist_duration">Durata • %1$s</string>
<string name="playlist_song_count">Playlist • %1$d brani</string> <string name="playlist_editor_dialog_action_delete_toast">Premi a lungo per eliminare</string>
<string name="podcast_bottom_sheet_add_to_queue">Aggiungi alla coda</string> <string name="playlist_editor_dialog_hint_name">Nome della playlist</string>
<string name="podcast_bottom_sheet_delete">Elimina</string> <string name="playlist_editor_dialog_negative_button">Annulla</string>
<string name="podcast_bottom_sheet_download">Scarica</string> <string name="playlist_editor_dialog_neutral_button">Elimina</string>
<string name="podcast_bottom_sheet_go_to_channel">Vai al canale</string> <string name="playlist_editor_dialog_positive_button">Salva</string>
<string name="podcast_bottom_sheet_play_next">Riproduci dopo</string> <string name="playlist_editor_dialog_title">Modifica playlist</string>
<string name="podcast_bottom_sheet_remove">Rimuovi</string> <string name="playlist_page_play_button">Riproduci</string>
<string name="podcast_channel_catalogue_title">Canali</string> <string name="playlist_page_shuffle_button">Mescola</string>
<string name="podcast_channel_catalogue_title_expanded">Sfoglia Canali</string> <string name="playlist_song_count">Playlist • %1$d brani</string>
<string name="podcast_channel_editor_dialog_hint_rss_url">URL RSS</string> <string name="podcast_bottom_sheet_add_to_queue">Aggiungi alla coda</string>
<string name="podcast_channel_editor_dialog_title">Canale Podcast</string> <string name="podcast_bottom_sheet_delete">Elimina</string>
<string name="podcast_channel_page_title_description_section">Descrizione</string> <string name="podcast_bottom_sheet_download">Scarica</string>
<string name="podcast_channel_page_title_episode_section">Episodi</string> <string name="podcast_bottom_sheet_go_to_channel">Vai al canale</string>
<string name="podcast_channel_page_title_no_episode_available">Nessun episodio disponibile</string> <string name="podcast_bottom_sheet_play_next">Riproduci dopo</string>
<string name="podcast_episode_download_request_snackbar">La tua richiesta è stata inviata al server</string> <string name="podcast_bottom_sheet_remove">Rimuovi</string>
<string name="podcast_info_empty_button">Clicca per nascondere la sezione\nGli effetti saranno visibili al riavvio</string> <string name="podcast_channel_catalogue_title">Canali</string>
<string name="podcast_info_empty_subtitle">Una volta aggiunto un canale, lo troverai qui</string> <string name="podcast_channel_catalogue_title_expanded">Sfoglia Canali</string>
<string name="podcast_info_empty_title">Nessun podcast trovato!</string> <string name="podcast_channel_editor_dialog_hint_rss_url">URL RSS</string>
<string name="podcast_release_date_duration_formatter">%1$s • %2$s</string> <string name="podcast_channel_editor_dialog_title">Canale Podcast</string>
<string name="radio_editor_dialog_hint_homepage_url">URL Homepage Radio</string> <string name="podcast_channel_page_title_description_section">Descrizione</string>
<string name="radio_editor_dialog_hint_name">Nome Radio</string> <string name="podcast_channel_page_title_episode_section">Episodi</string>
<string name="radio_editor_dialog_hint_stream_url">URL Stream Radio</string> <string name="podcast_channel_page_title_no_episode_available">Nessun episodio disponibile</string>
<string name="radio_editor_dialog_negative_button">Annulla</string> <string name="podcast_episode_download_request_snackbar">La tua richiesta è stata inviata al server</string>
<string name="radio_editor_dialog_neutral_button">Elimina</string> <string name="podcast_info_empty_button">Clicca per nascondere la sezione\nGli effetti saranno visibili al riavvio</string>
<string name="radio_editor_dialog_positive_button">Salva</string> <string name="podcast_info_empty_subtitle">Una volta aggiunto un canale, lo troverai qui</string>
<string name="radio_editor_dialog_title">Stazione Radio Internet</string> <string name="podcast_info_empty_title">Nessun podcast trovato!</string>
<string name="radio_station_info_empty_button">Clicca per nascondere la sezione\nGli effetti saranno visibili al riavvio</string> <string name="podcast_release_date_duration_formatter">%1$s • %2$s</string>
<string name="radio_station_info_empty_subtitle">Una volta aggiunta una stazione radio, la troverai qui</string> <string name="radio_editor_dialog_hint_homepage_url">URL Homepage Radio</string>
<string name="radio_station_info_empty_title">Nessuna stazione trovata!</string> <string name="radio_editor_dialog_hint_name">Nome Radio</string>
<string name="rating_dialog_negative_button">Annulla</string> <string name="radio_editor_dialog_hint_stream_url">URL Stream Radio</string>
<string name="rating_dialog_positive_button">Salva</string> <string name="radio_editor_dialog_negative_button">Annulla</string>
<string name="rating_dialog_title">Valuta</string> <string name="radio_editor_dialog_neutral_button">Elimina</string>
<string name="search_hint">Cerca titolo, artisti o album</string> <string name="radio_editor_dialog_positive_button">Salva</string>
<string name="search_info_minimum_characters">Inserisci almeno tre caratteri</string> <string name="radio_editor_dialog_title">Stazione Internet-Radio</string>
<string name="search_title_album">Album</string> <string name="radio_station_info_empty_button">Clicca per nascondere la sezione\nGli effetti saranno visibili al riavvio</string>
<string name="search_title_artist">Artisti</string> <string name="radio_station_info_empty_subtitle">Una volta aggiunta una stazione radio, la troverai qui</string>
<string name="search_title_song">Brani</string> <string name="radio_station_info_empty_title">Nessuna stazione trovata!</string>
<string name="server_signup_dialog_action_low_security">Bassa sicurezza</string> <string name="rating_dialog_negative_button">Annulla</string>
<string name="server_signup_dialog_action_delete_toast">Premi a lungo per eliminare</string> <string name="rating_dialog_positive_button">Salva</string>
<string name="server_signup_dialog_hint_local_address">URL locale</string> <string name="rating_dialog_title">Valuta</string>
<string name="server_signup_dialog_hint_name">Nome Server</string> <string name="search_hint">Cerca titolo, artisti o album</string>
<string name="server_signup_dialog_hint_password">Password</string> <string name="search_info_minimum_characters">Inserisci almeno tre caratteri</string>
<string name="server_signup_dialog_hint_url">URL Server</string> <string name="search_title_album">Album</string>
<string name="server_signup_dialog_hint_username">Nome utente</string> <string name="search_title_artist">Artisti</string>
<string name="server_signup_dialog_negative_button">Annulla</string> <string name="search_title_song">Brani</string>
<string name="server_signup_dialog_neutral_button">Elimina</string> <string name="server_signup_dialog_action_low_security">Bassa sicurezza</string>
<string name="server_signup_dialog_positive_button">Salva</string> <string name="server_signup_dialog_action_delete_toast">Premi a lungo per eliminare</string>
<string name="server_signup_dialog_title">Aggiungi server</string> <string name="server_signup_dialog_hint_local_address">URL locale</string>
<string name="server_unreachable_dialog_negative_button">Annulla</string> <string name="server_signup_dialog_hint_name">Nome Server</string>
<string name="server_unreachable_dialog_neutral_button">Vai al login</string> <string name="server_signup_dialog_hint_password">Password</string>
<string name="server_unreachable_dialog_positive_button">Continua comunque</string> <string name="server_signup_dialog_hint_url">URL Server</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_signup_dialog_hint_username">Nome utente</string>
<string name="server_unreachable_dialog_title">Server irraggiungibile</string> <string name="server_signup_dialog_negative_button">Annulla</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="server_signup_dialog_neutral_button">Elimina</string>
<string name="settings_about_title">Informazioni</string> <string name="server_signup_dialog_positive_button">Salva</string>
<string name="settings_always_on_display">Sempre attivo</string> <string name="server_signup_dialog_title">Aggiungi server</string>
<string name="settings_audio_transcode_download_format">Formato transcodifica</string> <string name="server_unreachable_dialog_negative_button">Annulla</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="server_unreachable_dialog_neutral_button">Vai al login</string>
<string name="settings_audio_transcode_download_priority_title">Dare priorità alle impostazioni del server per lo streaming nei download</string> <string name="server_unreachable_dialog_positive_button">Continua comunque</string>
<string name="settings_audio_transcode_download_summary">Se abilitato, Tempus scaricherà i brani transcodificati.</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="settings_audio_transcode_download_title">Scarica brani transcodificati</string> <string name="server_unreachable_dialog_title">Server irraggiungibile</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_about_summary">Tempus è un client musicale open source e leggero per Subsonic, progettato e costruito nativamente per Android.</string>
<string name="settings_audio_transcode_estimate_content_length_title">Stima della lunghezza del contenuto</string> <string name="settings_about_title">Informazioni</string>
<string name="settings_audio_transcode_format_download">Formato transcodifica per download</string> <string name="settings_always_on_display">Sempre attivo</string>
<string name="settings_audio_transcode_format_mobile">Formato transcodifica su mobile</string> <string name="settings_allow_playlist_duplicates">Allow adding duplicates to playlist</string>
<string name="settings_audio_transcode_format_wifi">Formato transcodifica su Wi-Fi</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_priority_summary">Se abilitato, Tempus non forzerà lo streaming del brano con le impostazioni di transcodifica sottostanti.</string> <string name="settings_audio_transcode_download_format">Formato transcodifica</string>
<string name="settings_audio_transcode_priority_title">Dare priorità alle impostazioni di transcodifica del server</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_priority_toast">Priorità di transcodifica del brano assegnata al server</string> <string name="settings_audio_transcode_download_priority_title">Dare priorità alle impostazioni del server per lo streaming nei download</string>
<string name="settings_buffering_strategy">Strategia di buffering</string> <string name="settings_audio_transcode_download_summary">Se abilitato, Tempus scaricherà i brani transcodificati.</string>
<string name="settings_buffering_strategy_summary">Perché la modifica abbia effetto è necessario riavviare manualmente l\'app.</string> <string name="settings_audio_transcode_download_title">Scarica brani transcodificati</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_audio_transcode_estimate_content_length_summary">Se abilitato, verrà richiesto al server di fornire la durata stimata del brano.</string>
<string name="settings_continuous_play_title">Riproduzione continua</string> <string name="settings_audio_transcode_estimate_content_length_title">Stima della lunghezza del contenuto</string>
<string name="settings_covers_cache">Dimensione della cache delle copertine</string> <string name="settings_audio_transcode_format_download">Formato transcodifica per download</string>
<string name="settings_data_saving_mode_summary">Per ridurre il consumo di dati, evita di scaricare le copertine.</string> <string name="settings_audio_transcode_format_mobile">Formato transcodifica su mobile</string>
<string name="settings_data_saving_mode_title">Limita utilizzo dei dati mobili</string> <string name="settings_audio_transcode_format_wifi">Formato transcodifica su Wi-Fi</string>
<string name="settings_delete_download_storage_summary">Continuando, tutti gli elementi salvati verranno eliminati in modo irreversibile.</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_delete_download_storage_title">Elimina elementi salvati</string> <string name="settings_audio_transcode_priority_title">Dare priorità alle impostazioni di transcodifica del server</string>
<string name="settings_download_storage_title">Archivio download</string> <string name="settings_audio_transcode_priority_toast">Priorità di transcodifica del brano assegnata al server</string>
<string name="settings_system_equalizer_summary">Regola le impostazioni audio</string> <string name="settings_buffering_strategy">Strategia di buffering</string>
<string name="settings_system_equalizer_title">Equalizzatore di sistema</string> <string name="settings_buffering_strategy_summary">Perché la modifica abbia effetto è necessario riavviare manualmente l\'app.</string>
<string name="settings_github_link">https://github.com/eddyizm/tempus</string> <string name="settings_choose_download_folder">Scegli una cartella dove scaricare la musica</string>
<string name="settings_github_summary">Segui lo sviluppo</string> <string name="settings_clear_download_folder">Svuota la cartella di download</string>
<string name="settings_github_title">Github</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_image_size">Imposta risoluzione delle immagini</string> <string name="settings_continuous_play_title">Riproduzione continua</string>
<string name="settings_language">Lingua</string> <string name="settings_covers_cache">Dimensione della cache delle copertine</string>
<string name="settings_logout_title">Esci</string> <string name="settings_data_saving_mode_summary">Per ridurre il consumo di dati, evita di scaricare le copertine.</string>
<string name="settings_max_bitrate_download">Bitrate per download</string> <string name="settings_data_saving_mode_title">Limita utilizzo dei dati mobili</string>
<string name="settings_max_bitrate_mobile">Bitrate su mobile</string> <string name="settings_delete_download_storage_summary">Continuando, tutti gli elementi salvati verranno eliminati in modo irreversibile.</string>
<string name="settings_max_bitrate_wifi">Bitrate su Wi-Fi</string> <string name="settings_delete_download_storage_title">Elimina elementi salvati</string>
<string name="settings_media_cache">Dimensione della cache dei file multimediali</string> <string name="settings_download_storage_title">Archivio download</string>
<string name="settings_music_directory">Mostra directory musicali</string> <string name="settings_download_folder_cleared">Cartella di download svuotata.</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_download_folder_set">Cartella di download impostata</string>
<string name="settings_podcast">Mostra podcast</string> <string name="settings_set_download_folder">Imposta cartella di download</string>
<string name="settings_podcast_summary">Se abilitato, mostra la sezione podcast. Riavvia l\'app per rendere effettive le modifiche.</string> <string name="settings_system_equalizer_summary">Regola le impostazioni audio</string>
<string name="settings_audio_quality">Mostra qualità audio</string> <string name="settings_system_equalizer_title">Equalizzatore di sistema</string>
<string name="settings_audio_quality_summary">Il bitrate e il formato audio saranno mostrati per ogni traccia.</string> <string name="settings_github_link">https://github.com/eddyizm/tempus</string>
<string name="settings_item_rating">Mostra valutazione</string> <string name="settings_github_summary">Segui lo sviluppo</string>
<string name="settings_item_rating_summary">Se abilitato, verrà mostrata la valutazione dell\'elemento e se è contrassegnato come preferito.</string> <string name="settings_github_title">Github</string>
<string name="settings_queue_syncing_countdown">Timer sincronizzazione</string> <string name="settings_support_discussion_link">https://github.com/eddyizm/tempus/discussions</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_github_update">Aggiornamenti</string>
<string name="settings_queue_syncing_title">Sincronizza coda di riproduzione per questo utente</string> <string name="settings_github_update_title">Controlla GitHub per aggiornamenti</string>
<string name="settings_radio">Mostra radio</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_radio_summary">Se abilitato, mostra la sezione radio. Riavvia l\'app per applicare completamente le modifiche.</string> <string name="settings_support_summary">Partecipa alle discussioni della community e al supporto</string>
<string name="settings_replay_gain">Imposta modalità di guadagno di riproduzione</string> <string name="settings_support_title">Supporto utenti</string>
<string name="settings_rounded_corner">Angoli arrotondati</string> <string name="settings_scan_result">Scansione: conteggio di %1$d brani</string>
<string name="settings_rounded_corner_size">Dimensione angoli</string> <string name="settings_image_size">Imposta risoluzione delle immagini</string>
<string name="settings_rounded_corner_size_summary">Imposta la magnitudine dell\'angolo di curvatura.</string> <string name="settings_language">Lingua</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_logout_title">Esci</string>
<string name="settings_scan_title">Scansiona libreria</string> <string name="settings_max_bitrate_download">Bitrate per download</string>
<string name="settings_scrobble_title">Abilita scrobbling musicale</string> <string name="settings_max_bitrate_mobile">Bitrate su mobile</string>
<string name="settings_share_title">Abilita condivisione musicale</string> <string name="settings_max_bitrate_wifi">Bitrate su Wi-Fi</string>
<string name="settings_streaming_cache_size">Dimensione cache streaming</string> <string name="settings_media_cache">Dimensione della cache dei file multimediali</string>
<string name="settings_streaming_cache_storage_title">Archiviazione cache streaming</string> <string name="settings_music_directory">Mostra directory musicali</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_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_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_podcast">Mostra podcast</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_podcast_summary">Se abilitato, mostra la sezione podcast. Riavvia l\'app per rendere effettive le modifiche.</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_audio_quality">Mostra qualità audio</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_audio_quality_summary">Il bitrate e il formato audio saranno mostrati per ogni traccia.</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_song_rating">Mostra valutazione della canzone</string>
<string name="settings_summary_streaming_cache_size">%1$s \nAttualmente in uso: %2$s MiB</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_summary_transcoding">Priorità data alla modalità di transcoding. Se impostato su "Riproduzione diretta", il bitrate del file non verrà modificato.</string> <string name="settings_item_rating">Mostra valutazione</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_item_rating_summary">Se abilitato, verrà mostrata la valutazione dell\'elemento e se è contrassegnato come preferito.</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_queue_syncing_countdown">Timer sincronizzazione</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_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_sync_starred_tracks_for_offline_use_title">Sincronizza tracce contrassegnate per uso offline</string> <string name="settings_queue_syncing_title">Sincronizza coda di riproduzione per questo utente [Not Fully Baked]</string>
<string name="settings_theme">Tema</string> <string name="settings_show_mini_shuffle_button">Mostra il pulsante di riproduzione casuale</string>
<string name="settings_title_data">Dati</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_title_general">Generale</string> <string name="settings_radio">Mostra radio</string>
<string name="settings_title_rating">Valutazione</string> <string name="settings_radio_summary">Se abilitato, mostra la sezione radio. Riavvia l\'app per applicare completamente le modifiche.</string>
<string name="settings_title_replay_gain">Guadagno di riproduzione</string> <string name="settings_auto_download_lyrics">Scarica automaticamente i testi</string>
<string name="settings_title_scrobble">Scrobble</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_title_skip_min_star_rating">Ignora brani in base alla valutazione</string> <string name="settings_replay_gain">Imposta modalità di guadagno di riproduzione</string>
<string name="settings_title_skip_min_star_rating_dialog">Brani con una valutazione di:</string> <string name="settings_rounded_corner">Angoli arrotondati</string>
<string name="settings_title_share">Condividi</string> <string name="settings_rounded_corner_size">Dimensione angoli</string>
<string name="settings_title_syncing">Sincronizzazione</string> <string name="settings_rounded_corner_size_summary">Imposta la grandezza dell\'angolo di curvatura.</string>
<string name="settings_title_transcoding">Transcoding</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_title_transcoding_download">Download di Transcoding</string> <string name="settings_scan_title">Scansiona libreria</string>
<string name="settings_title_ui">Interfaccia utente</string> <string name="settings_scrobble_title">Abilita scrobbling musicale</string>
<string name="settings_transcoded_download">Download transcodificato</string> <string name="settings_system_language">Lingua di sistema</string>
<string name="settings_version_summary" translatable="false">3.1.0</string> <string name="settings_share_title">Abilita condivisione musicale</string>
<string name="settings_version_title">Versione</string> <string name="settings_streaming_cache_size">Dimensione cache streaming</string>
<string name="settings_wifi_only_summary">Chiedi conferma all\'utente prima di effettuare streaming su rete mobile.</string> <string name="settings_streaming_cache_storage_title">Archiviazione cache streaming</string>
<string name="settings_wifi_only_title">Streaming solo tramite Wi-Fi avviso</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="share_bottom_sheet_copy_link">Copia link</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="share_bottom_sheet_delete">Elimina condivisione</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="share_bottom_sheet_update">Aggiorna condivisione</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="share_subtitle_item">Data di scadenza: %1$s</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="share_unsupported_error">La condivisione non è supportata o non è abilitata</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="share_update_dialog_hint_description">Descrizione</string> <string name="settings_summary_streaming_cache_size">%1$s \nAttualmente in uso: %2$s MiB</string>
<string name="share_update_dialog_hint_expiration_date">Data di scadenza</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="share_update_dialog_negative_button">Annulla</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="share_update_dialog_positive_button">Salva</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="share_update_dialog_title">Condividi</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="song_bottom_sheet_add_to_playlist">Aggiungi alla playlist</string> <string name="settings_sync_starred_artists_for_offline_use_title">Sincronizza artisti preferiti per uso offline</string>
<string name="song_bottom_sheet_add_to_queue">Aggiungi alla coda</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="song_bottom_sheet_download">Scarica</string> <string name="settings_sync_starred_albums_for_offline_use_title">Sincronizza album preferiti per uso offline</string>
<string name="song_bottom_sheet_error_retrieving_album">Errore nel recupero dell\'album</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="song_bottom_sheet_error_retrieving_artist">Errore nel recupero dell\'artista</string> <string name="settings_sync_starred_tracks_for_offline_use_title">Sincronizza tracce preferite per uso offline</string>
<string name="song_bottom_sheet_go_to_album">Vai all\'album</string> <string name="settings_theme">Tema</string>
<string name="song_bottom_sheet_go_to_artist">Vai all\'artista</string> <string name="settings_title_data">Dati</string>
<string name="song_bottom_sheet_instant_mix">Mix istantaneo</string> <string name="settings_title_general">Generale</string>
<string name="song_bottom_sheet_play_next">Riproduci dopo</string> <string name="settings_title_playlist">Playlist</string>
<string name="song_bottom_sheet_rate">Valuta</string> <string name="settings_title_rating">Valutazione</string>
<string name="song_bottom_sheet_remove">Rimuovi</string> <string name="settings_title_replay_gain">Guadagno di riproduzione</string>
<string name="song_bottom_sheet_share">Condividi</string> <string name="settings_title_scrobble">Scrobble</string>
<string name="song_list_page_downloaded">Scaricato</string> <string name="settings_title_skip_min_star_rating">Ignora brani in base alla valutazione</string>
<string name="song_list_page_most_played">Tracce più riprodotte</string> <string name="settings_title_skip_min_star_rating_dialog">Brani con una valutazione di:</string>
<string name="song_list_page_recently_added">Tracce aggiunte di recente</string> <string name="settings_title_share">Condividi</string>
<string name="song_list_page_recently_played">Tracce riprodotte di recente</string> <string name="settings_title_syncing">Sincronizzazione</string>
<string name="song_list_page_starred">Tracce contrassegnate</string> <string name="settings_title_transcoding">Transcodifica</string>
<string name="song_list_page_top">Le migliori tracce di %1$s</string> <string name="settings_title_transcoding_download">Transcodifica dei Download</string>
<string name="song_list_page_year">Anno %1$d</string> <string name="settings_title_ui">Interfaccia utente</string>
<string name="song_subtitle_formatter">%1$s • %2$s %3$s</string> <string name="settings_transcoded_download">Download transcodificato</string>
<string name="starred_sync_dialog_negative_button">Annulla</string> <string name="settings_version_summary" translatable="false">3.1.0</string>
<string name="starred_sync_dialog_neutral_button">Continua</string> <string name="settings_version_title">Versione</string>
<string name="starred_sync_dialog_positive_button">Continua e scarica</string> <string name="settings_wifi_only_summary">Chiedi conferma all\'utente prima di effettuare streaming su rete mobile.</string>
<string name="starred_sync_dialog_summary">Il download delle tracce contrassegnate potrebbe richiedere una grande quantità di dati.</string> <string name="settings_wifi_only_title">Streaming solo tramite Wi-Fi avviso</string>
<string name="starred_sync_dialog_title">Sincronizza tracce contrassegnate</string> <string name="share_bottom_sheet_copy_link">Copia link</string>
<string name="streaming_cache_storage_dialog_sub_summary">Per rendere effettive le modifiche, riavvia l\'app.</string> <string name="share_bottom_sheet_delete">Elimina condivisione</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="share_bottom_sheet_update">Aggiorna condivisione</string>
<string name="streaming_cache_storage_dialog_title">Seleziona opzione di archiviazione</string> <string name="share_subtitle_item">Data di scadenza: %1$s</string>
<string name="streaming_cache_storage_external_dialog_positive_button">Esterno</string> <string name="share_no_expiration">Mai</string>
<string name="streaming_cache_storage_internal_dialog_negative_button">Interno</string> <string name="share_unsupported_error">La condivisione non è supportata o non è abilitata</string>
<string name="support_url">https://buymeacoffee.com/a.cappiello</string> <string name="asset_link_clipboard_label">Link asset Tempus</string>
<string name="track_info_album">Album</string> <string name="asset_link_label_song">UID Canzone</string>
<string name="track_info_artist">Artista</string> <string name="asset_link_label_album">UID Album</string>
<string name="track_info_bitrate">Bitrate</string> <string name="asset_link_label_artist">UID Artista</string>
<string name="track_info_content_type">Tipo di contenuto</string> <string name="asset_link_label_playlist">UID Playlist</string>
<string name="track_info_dialog_positive_button">OK</string> <string name="asset_link_label_genre">UID Genere</string>
<string name="track_info_dialog_title">Info traccia</string> <string name="asset_link_label_year">UID Anno</string>
<string name="track_info_disc_number">Numero del disco</string> <string name="asset_link_label_unknown">UID Asset</string>
<string name="track_info_duration">Durata</string> <string name="asset_link_error_unsupported">Link asset non supportato</string>
<string name="track_info_genre">Genere</string> <string name="asset_link_error_song">Impossibile aprire la canzone</string>
<string name="track_info_path">Percorso</string> <string name="asset_link_error_album">Impossibile aprire l\'album</string>
<string name="track_info_size">Dimensione</string> <string name="asset_link_error_artist">Impossibile aprire l\'artista</string>
<string name="track_info_suffix">Suffisso</string> <string name="asset_link_error_playlist">Impossibile aprire la playlist</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="asset_link_chip_text">%1$s • %2$s</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="asset_link_copied_toast">Copiato %1$s negli appunti</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="asset_link_debug_toast">Link asset: %1$s</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="share_update_dialog_hint_description">Descrizione</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="share_update_dialog_hint_expiration_date">Data di scadenza</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="share_update_dialog_negative_button">Annulla</string>
<string name="track_info_title">Titolo</string> <string name="share_update_dialog_positive_button">Salva</string>
<string name="track_info_track_number">Numero traccia</string> <string name="share_update_dialog_title">Condividi</string>
<string name="track_info_transcoded_content_type">Tipo di contenuto transcodedato</string> <string name="song_bottom_sheet_add_to_playlist">Aggiungi alla playlist</string>
<string name="track_info_transcoded_suffix">Suffisso transcodedato</string> <string name="song_bottom_sheet_add_to_queue">Aggiungi alla coda</string>
<string name="track_info_year">Anno</string> <string name="song_bottom_sheet_download">Scarica</string>
<string name="undraw_page">unDraw</string> <string name="song_bottom_sheet_error_retrieving_album">Errore nel recupero dell\'album</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="song_bottom_sheet_error_retrieving_artist">Errore nel recupero dell\'artista</string>
<string name="undraw_url">https://undraw.co/</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> </resources>

View file

@ -1,182 +1,18 @@
package com.cappielloantonio.tempo.service 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.core.content.ContextCompat
import androidx.media3.cast.CastPlayer import androidx.media3.cast.CastPlayer
import androidx.media3.cast.SessionAvailabilityListener 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.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.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.cast.framework.CastContext
import com.google.android.gms.common.ConnectionResult import com.google.android.gms.common.ConnectionResult
import com.google.android.gms.common.GoogleApiAvailability import com.google.android.gms.common.GoogleApiAvailability
@UnstableApi @UnstableApi
class MediaService : MediaLibraryService(), SessionAvailabilityListener { class MediaService : BaseMediaService(), SessionAvailabilityListener {
private lateinit var automotiveRepository: AutomotiveRepository private val automotiveRepository = AutomotiveRepository()
private lateinit var player: ExoPlayer
private lateinit var castPlayer: CastPlayer 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") @Suppress("DEPRECATION")
private fun initializeCastPlayer() { private fun initializeCastPlayer() {
@ -184,284 +20,41 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
.isGooglePlayServicesAvailable(this) == ConnectionResult.SUCCESS .isGooglePlayServicesAvailable(this) == ConnectionResult.SUCCESS
) { ) {
CastContext.getSharedInstance(this, ContextCompat.getMainExecutor(this)) CastContext.getSharedInstance(this, ContextCompat.getMainExecutor(this))
.addOnSuccessListener { castContext -> .addOnSuccessListener { castContext ->
castPlayer = CastPlayer(castContext) castPlayer = CastPlayer(castContext)
castPlayer.setSessionAvailabilityListener(this@MediaService) castPlayer.setSessionAvailabilityListener(this@MediaService)
initializePlayerListener(castPlayer)
if (castPlayer.isCastSessionAvailable && this::mediaLibrarySession.isInitialized) { if (castPlayer.isCastSessionAvailable)
setPlayer(player, castPlayer) setPlayer(mediaLibrarySession.player, castPlayer)
} }
}
} }
} }
private fun initializeMediaLibrarySession() { override fun getMediaLibrarySessionCallback(): MediaLibrarySession.Callback {
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 {
return MediaLibrarySessionCallback(this, automotiveRepository) return MediaLibrarySessionCallback(this, automotiveRepository)
} }
private fun initializePlayerListener() { override fun playerInitHook() {
player.addListener(object : Player.Listener { super.playerInitHook()
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) { initializeCastPlayer()
if (mediaItem == null) return if (this::castPlayer.isInitialized && castPlayer.isCastSessionAvailable)
setPlayer(null, castPlayer)
}
if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_SEEK || reason == Player.MEDIA_ITEM_TRANSITION_REASON_AUTO) { override fun releasePlayers() {
MediaManager.setLastPlayedTimestamp(mediaItem) if (this::castPlayer.isInitialized) {
} castPlayer.setSessionAvailabilityListener(null)
updateWidget() castPlayer.release()
}
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()
} }
}
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() automotiveRepository.deleteMetadata()
super.releasePlayers()
} }
private fun releaseNetworkCallback() {
getSystemService(ConnectivityManager::class.java).unregisterNetworkCallback(networkCallback)
}
private fun getRenderersFactory() = DownloadUtil.buildRenderersFactory(this, false)
override fun onCastSessionAvailable() { override fun onCastSessionAvailable() {
val currentQueue = getQueueFromPlayer(player) setPlayer(exoplayer, castPlayer)
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()
} }
override fun onCastSessionUnavailable() { override fun onCastSessionUnavailable() {
val currentQueue = getQueueFromPlayer(castPlayer) setPlayer(castPlayer, exoplayer)
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()
} }
}
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

View file

@ -1,5 +1,5 @@
Update russian strings.xml by @Sevinfolds in https://github.com/eddyizm/tempus/pull/249 Update russian strings.xml
Disallow duplicate songs in queue by @eddyizm in https://github.com/eddyizm/tempus/pull/252 Disallow duplicate songs in queue
Fixed crash when viewing share by @drakeerv in https://github.com/eddyizm/tempus/pull/255 Fixed crash when viewing share
Update Polish translation by @skajmer in https://github.com/eddyizm/tempus/pull/257 Update Polish translation
Add podcast channel visible when empty podcasts by @eddyizm in https://github.com/eddyizm/tempus/pull/260 Add podcast channel visible when empty podcasts

View file

@ -0,0 +1,5 @@
* fix: Fix player queue soft-lock
* chore: Add Catalan i18n
* chore: Refactor MediaService
* chore(i18n): Update Spanish translation
* chore(i18n): Update Italian translation