mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-10 23:21:59 +01:00
address review: localize connection errors, tighten i18n guard, drop dead resources
- AddSourceViewModel resolves SubsonicException.uiMessage too, so malformed/ HTTP/truncated responses show localized text instead of the raw parser message; add a regression test. - StringResourcesTest now requires an 'other' quantity in every plural and asserts exact placeholder (index and conversion) equality, both of which could previously pass while crashing getQuantityString at runtime. - Map the pre-enum song_downloads.error sentences back to their DownloadError so existing failed rows keep their specific message. - Localize the media-session star/unstar button label. - Remove 12 unreferenced resources (queue name, filter year, sync_failed, and the unused codec_* strings; codec labels stay in AudioEncoding). - Make buildTree's fallback arguments required; only tests used the defaults.
This commit is contained in:
@@ -130,5 +130,25 @@ class DownloadErrorConverter {
|
||||
fun fromDownloadError(error: DownloadError): String = error.name
|
||||
|
||||
@ColumnTypeConverter
|
||||
fun toDownloadError(value: String): DownloadError = DownloadError.entries.firstOrNull { it.name == value } ?: DownloadError.Failed
|
||||
fun toDownloadError(value: String): DownloadError =
|
||||
DownloadError.entries.firstOrNull { it.name == value }
|
||||
?: LEGACY_DOWNLOAD_ERRORS[value]
|
||||
?: DownloadError.Failed
|
||||
}
|
||||
|
||||
private val LEGACY_DOWNLOAD_ERRORS =
|
||||
mapOf(
|
||||
"Not enough space to download" to DownloadError.NoSpace,
|
||||
"Storage is unavailable" to DownloadError.StorageUnavailable,
|
||||
"Could not write the download" to DownloadError.WriteFailed,
|
||||
"The server refused to send this track" to DownloadError.ServerRefused,
|
||||
"The server redirected too many times" to DownloadError.TooManyRedirects,
|
||||
"The download could not be resumed" to DownloadError.ResumeFailed,
|
||||
"Download failed" to DownloadError.Failed,
|
||||
"The downloaded file is missing" to DownloadError.FileMissing,
|
||||
"The server sent an error instead of the track" to DownloadError.ServerError,
|
||||
"Can't download: the server address is unavailable" to DownloadError.NoAddress,
|
||||
"Nothing left to download from this list" to DownloadError.NothingLeft,
|
||||
"The download stopped unexpectedly" to DownloadError.Stopped,
|
||||
"Can't download from a source that isn't active" to DownloadError.InactiveSource,
|
||||
)
|
||||
|
||||
@@ -2,7 +2,6 @@ package com.subtracks.data.sync
|
||||
|
||||
import android.util.Log
|
||||
import com.subtracks.R
|
||||
import com.subtracks.UiException
|
||||
import com.subtracks.UiMessage
|
||||
import com.subtracks.data.db.SubtracksDatabase
|
||||
import com.subtracks.data.repo.QueueRepository
|
||||
@@ -73,10 +72,6 @@ class SyncManager(
|
||||
UiMessage(R.string.error_no_server)
|
||||
}
|
||||
|
||||
is UiException -> {
|
||||
failure.uiMessage
|
||||
}
|
||||
|
||||
else -> {
|
||||
(failure as? SubsonicException)?.uiMessage
|
||||
?: UiMessage(R.string.sync_failed_detail, listOf(failure.message ?: ""))
|
||||
|
||||
@@ -28,6 +28,7 @@ import coil3.SingletonImageLoader
|
||||
import com.google.common.util.concurrent.Futures
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import com.subtracks.MainActivity
|
||||
import com.subtracks.R
|
||||
import com.subtracks.data.repo.LibraryRepository
|
||||
import com.subtracks.data.repo.SourceRepository
|
||||
import com.subtracks.data.source.StarType
|
||||
@@ -176,7 +177,7 @@ class PlaybackService : MediaSessionService() {
|
||||
CommandButton
|
||||
.Builder(if (isStarred) CommandButton.ICON_STAR_FILLED else CommandButton.ICON_STAR_UNFILLED)
|
||||
.setSessionCommand(STAR_COMMAND)
|
||||
.setDisplayName(if (isStarred) "Unstar" else "Star")
|
||||
.setDisplayName(getString(if (isStarred) R.string.actions_unstar else R.string.actions_star))
|
||||
.build()
|
||||
|
||||
private fun streamingClient(): OkHttpClient =
|
||||
|
||||
@@ -93,8 +93,8 @@ class DownloadsViewModel(
|
||||
|
||||
internal fun buildTree(
|
||||
songs: List<DownloadedSong>,
|
||||
unknownArtist: String = "Unknown artist",
|
||||
unknownAlbum: String = "Unknown album",
|
||||
unknownArtist: String,
|
||||
unknownAlbum: String,
|
||||
): DownloadTree =
|
||||
DownloadTree(
|
||||
artists =
|
||||
|
||||
@@ -7,6 +7,7 @@ import com.subtracks.R
|
||||
import com.subtracks.UiException
|
||||
import com.subtracks.data.repo.DownloadRepository
|
||||
import com.subtracks.data.repo.SourceRepository
|
||||
import com.subtracks.data.source.subsonic.SubsonicException
|
||||
import com.subtracks.data.sync.SyncManager
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
@@ -101,8 +102,8 @@ class AddSourceViewModel(
|
||||
}
|
||||
},
|
||||
{ error ->
|
||||
(error as? UiException)?.uiMessage?.resolve(resources)
|
||||
?: resources.getString(R.string.connection_failed, error.message)
|
||||
val ui = (error as? UiException)?.uiMessage ?: (error as? SubsonicException)?.uiMessage
|
||||
ui?.resolve(resources) ?: resources.getString(R.string.connection_failed, error.message)
|
||||
},
|
||||
),
|
||||
isError = result.isFailure,
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
<string name="resources_filter_artist">Artista</string>
|
||||
<string name="resources_filter_genre">Gênero</string>
|
||||
<string name="resources_filter_starred">Estrelado por</string>
|
||||
<string name="resources_filter_year">Ano</string>
|
||||
<plurals name="resources_song_count">
|
||||
<item quantity="one">%1$d song</item>
|
||||
<item quantity="other">%1$d songs</item>
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
<string name="resources_filter_artist">歌手</string>
|
||||
<string name="resources_filter_genre">根据类型</string>
|
||||
<string name="resources_filter_starred">已收藏</string>
|
||||
<string name="resources_filter_year">年份</string>
|
||||
<plurals name="resources_song_count">
|
||||
<item quantity="other">%1$d 首歌曲</item>
|
||||
</plurals>
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
<string name="resources_filter_artist">歌手</string>
|
||||
<string name="resources_filter_genre">根據類型</string>
|
||||
<string name="resources_filter_starred">已收藏</string>
|
||||
<string name="resources_filter_year">年份</string>
|
||||
<plurals name="resources_song_count">
|
||||
<item quantity="other">%1$d 首歌曲</item>
|
||||
</plurals>
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
<string name="resources_filter_artist">Umělec</string>
|
||||
<string name="resources_filter_genre">Podle žánru</string>
|
||||
<string name="resources_filter_starred">Ohodnocené</string>
|
||||
<string name="resources_filter_year">Rok</string>
|
||||
<string name="resources_sort_by_artist">Umělce</string>
|
||||
<string name="resources_sort_by_frequently_played">Často přehrávané</string>
|
||||
<string name="resources_sort_by_name">Názvu</string>
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
<string name="resources_filter_artist">Künstler</string>
|
||||
<string name="resources_filter_genre">Nach Genre</string>
|
||||
<string name="resources_filter_starred">Favoriten</string>
|
||||
<string name="resources_filter_year">Jahr</string>
|
||||
<plurals name="resources_song_count">
|
||||
<item quantity="one">%1$d Song</item>
|
||||
<item quantity="other">%1$d Songs</item>
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
<string name="resources_filter_artist">Artista</string>
|
||||
<string name="resources_filter_genre">Género</string>
|
||||
<string name="resources_filter_starred">Estrellas</string>
|
||||
<string name="resources_filter_year">Año</string>
|
||||
<plurals name="resources_song_count">
|
||||
<item quantity="one">%1$d Canción</item>
|
||||
<item quantity="other">%1$d Canciones</item>
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
<string name="resources_filter_artist">Musikaria</string>
|
||||
<string name="resources_filter_genre">Generoa</string>
|
||||
<string name="resources_filter_starred">Gogokoak</string>
|
||||
<string name="resources_filter_year">Urtea</string>
|
||||
<plurals name="resources_song_count">
|
||||
<item quantity="one">Kanta bat</item>
|
||||
<item quantity="other">%1$d kanta</item>
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
<string name="resources_filter_artist">Artiste</string>
|
||||
<string name="resources_filter_genre">Par Genre</string>
|
||||
<string name="resources_filter_starred">Favoris</string>
|
||||
<string name="resources_filter_year">Année</string>
|
||||
<plurals name="resources_song_count">
|
||||
<item quantity="one">%1$d chanson</item>
|
||||
<item quantity="other">%1$d chansons</item>
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
<string name="resources_filter_artist">Artista</string>
|
||||
<string name="resources_filter_genre">Por xénero</string>
|
||||
<string name="resources_filter_starred">Favoritas</string>
|
||||
<string name="resources_filter_year">Ano</string>
|
||||
<plurals name="resources_song_count">
|
||||
<item quantity="one">%1$d canción</item>
|
||||
<item quantity="other">%1$d cancións</item>
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
<string name="resources_filter_artist">Előadó</string>
|
||||
<string name="resources_filter_genre">Stílus</string>
|
||||
<string name="resources_filter_starred">Kedvencek között</string>
|
||||
<string name="resources_filter_year">Év</string>
|
||||
<string name="resources_sort_by_artist">Előadó</string>
|
||||
<string name="resources_sort_by_frequently_played">Gyakran játszott</string>
|
||||
<string name="resources_sort_by_name">Név</string>
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
<string name="resources_filter_artist">Artista</string>
|
||||
<string name="resources_filter_genre">Per genere</string>
|
||||
<string name="resources_filter_starred">Preferiti</string>
|
||||
<string name="resources_filter_year">Anno</string>
|
||||
<plurals name="resources_song_count">
|
||||
<item quantity="one">%1$d brano</item>
|
||||
<item quantity="other">%1$d brani</item>
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
<string name="navigation_tabs_settings">設定</string>
|
||||
<string name="resources_filter_album">アルブム</string>
|
||||
<string name="resources_filter_starred">星付きアルバム</string>
|
||||
<string name="resources_filter_year">年</string>
|
||||
<string name="resources_sort_by_frequently_played">よく聴くアルバム</string>
|
||||
<string name="resources_sort_by_recently_played">最近再生した</string>
|
||||
<string name="search_input_placeholder">検索</string>
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
<string name="resources_filter_artist">아티스트</string>
|
||||
<string name="resources_filter_genre">장르</string>
|
||||
<string name="resources_filter_starred">평가됨</string>
|
||||
<string name="resources_filter_year">년</string>
|
||||
<plurals name="resources_song_count">
|
||||
<item quantity="other">%1$d 노래</item>
|
||||
</plurals>
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
<string name="resources_filter_artist">Artist</string>
|
||||
<string name="resources_filter_genre">Etter sjanger</string>
|
||||
<string name="resources_filter_starred">Stjernemerket</string>
|
||||
<string name="resources_filter_year">År</string>
|
||||
<plurals name="resources_song_count">
|
||||
<item quantity="one">%1$d sang</item>
|
||||
<item quantity="other">%1$d sanger</item>
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
<string name="resources_filter_artist">Artysta</string>
|
||||
<string name="resources_filter_genre">Wg gatunku</string>
|
||||
<string name="resources_filter_starred">Ulubione</string>
|
||||
<string name="resources_filter_year">Rok</string>
|
||||
<string name="resources_sort_by_artist">Wg wykonawcy</string>
|
||||
<string name="resources_sort_by_frequently_played">Często odtwarzane</string>
|
||||
<string name="resources_sort_by_name">Wg nazwy</string>
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
<string name="resources_filter_artist">Artista</string>
|
||||
<string name="resources_filter_genre">Por Género</string>
|
||||
<string name="resources_filter_starred">Favoritos</string>
|
||||
<string name="resources_filter_year">Ano</string>
|
||||
<string name="resources_sort_by_artist">Por Artista</string>
|
||||
<string name="resources_sort_by_frequently_played">Mais Tocado</string>
|
||||
<string name="resources_sort_by_name">Por Nome</string>
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
<string name="resources_filter_artist">По исполнителю</string>
|
||||
<string name="resources_filter_genre">По жанру</string>
|
||||
<string name="resources_filter_starred">Избранные</string>
|
||||
<string name="resources_filter_year">По годам</string>
|
||||
<plurals name="resources_song_count">
|
||||
<item quantity="one">%1$d трек</item>
|
||||
<item quantity="few">%1$d треков</item>
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
<string name="resources_filter_artist">Artist</string>
|
||||
<string name="resources_filter_genre">Genre</string>
|
||||
<string name="resources_filter_starred">Markerad</string>
|
||||
<string name="resources_filter_year">År</string>
|
||||
<plurals name="resources_song_count">
|
||||
<item quantity="one">%1$d låt</item>
|
||||
<item quantity="other">%1$d låtar</item>
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
<string name="resources_filter_artist">கலைஞர்</string>
|
||||
<string name="resources_filter_genre">வகை</string>
|
||||
<string name="resources_filter_starred">நடித்தார்</string>
|
||||
<string name="resources_filter_year">ஆண்டு</string>
|
||||
<plurals name="resources_song_count">
|
||||
<item quantity="one">%1$d பாடல்</item>
|
||||
<item quantity="other">%1$d பாடல்கள்</item>
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
<string name="resources_filter_artist">ศิลปิน</string>
|
||||
<string name="resources_filter_genre">ประเภท</string>
|
||||
<string name="resources_filter_starred">ให้ดาว</string>
|
||||
<string name="resources_filter_year">ปี</string>
|
||||
<plurals name="resources_song_count">
|
||||
<item quantity="one">%1$d เพลง</item>
|
||||
<item quantity="other">%1$d เพลง</item>
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
<string name="resources_filter_artist">Sanatçı</string>
|
||||
<string name="resources_filter_genre">Türe göre</string>
|
||||
<string name="resources_filter_starred">Yıldızlı</string>
|
||||
<string name="resources_filter_year">Yıl</string>
|
||||
<plurals name="resources_song_count">
|
||||
<item quantity="one">%1$d şarkı</item>
|
||||
<item quantity="other">%1$d şarkı</item>
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
<string name="resources_filter_artist">Виконавець</string>
|
||||
<string name="resources_filter_genre">Жанр</string>
|
||||
<string name="resources_filter_starred">Улюблене</string>
|
||||
<string name="resources_filter_year">Рік</string>
|
||||
<plurals name="resources_song_count">
|
||||
<item quantity="one">%1$d пісня</item>
|
||||
<item quantity="other">%1$d пісні</item>
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
<string name="resources_filter_artist">Theo nghệ sĩ</string>
|
||||
<string name="resources_filter_genre">Theo thể loại</string>
|
||||
<string name="resources_filter_starred">Theo yêu thích</string>
|
||||
<string name="resources_filter_year">Theo năm</string>
|
||||
<plurals name="resources_song_count">
|
||||
<item quantity="one">%1$d bài hát</item>
|
||||
<item quantity="other">%1$d bài hát</item>
|
||||
|
||||
@@ -27,12 +27,10 @@
|
||||
<string name="resources_album_name">Albums</string>
|
||||
<string name="resources_artist_name">Artists</string>
|
||||
<string name="resources_playlist_name">Playlists</string>
|
||||
<string name="resources_queue_name">Queue</string>
|
||||
<string name="resources_filter_album">Album</string>
|
||||
<string name="resources_filter_artist">Artist</string>
|
||||
<string name="resources_filter_genre">Genre</string>
|
||||
<string name="resources_filter_starred">Starred</string>
|
||||
<string name="resources_filter_year">Year</string>
|
||||
<string name="resources_song_list_delete_all_content">%1$s\n\nDeleting will free %2$s</string>
|
||||
<string name="resources_song_list_delete_all_title">Delete downloads</string>
|
||||
<string name="resources_sort_by_added">Added</string>
|
||||
@@ -222,7 +220,6 @@
|
||||
<string name="star_update_failed">Could not update star</string>
|
||||
<string name="sync_offline">Offline mode is on</string>
|
||||
<string name="error_no_server">No server configured</string>
|
||||
<string name="sync_failed">Sync failed</string>
|
||||
<string name="sync_failed_detail">Sync failed: %1$s</string>
|
||||
|
||||
<string name="error_download_no_space">Not enough space to download</string>
|
||||
@@ -286,13 +283,4 @@
|
||||
<item quantity="other">%1$d plays</item>
|
||||
</plurals>
|
||||
|
||||
<string name="codec_mp3" translatable="false">MP3</string>
|
||||
<string name="codec_aac" translatable="false">AAC</string>
|
||||
<string name="codec_alac" translatable="false">ALAC</string>
|
||||
<string name="codec_flac" translatable="false">FLAC</string>
|
||||
<string name="codec_ogg" translatable="false">Ogg</string>
|
||||
<string name="codec_vorbis" translatable="false">Vorbis</string>
|
||||
<string name="codec_opus" translatable="false">Opus</string>
|
||||
<string name="codec_webm" translatable="false">WebM</string>
|
||||
<string name="codec_wav" translatable="false">WAV</string>
|
||||
</resources>
|
||||
|
||||
@@ -24,6 +24,12 @@ class StringResourcesTest {
|
||||
val default = parse(defaultFile)
|
||||
assertTrue("default strings.xml should not be empty", default.isNotEmpty())
|
||||
|
||||
default.forEach { (name, resource) ->
|
||||
if (resource.kind == "plurals") {
|
||||
assertTrue("$name in values/strings.xml is missing the required 'other' quantity", "other" in resource.quantities)
|
||||
}
|
||||
}
|
||||
|
||||
localeFiles().forEach { file ->
|
||||
val locale = parse(file)
|
||||
locale.forEach { (name, resource) ->
|
||||
@@ -35,12 +41,12 @@ class StringResourcesTest {
|
||||
"$name has an invalid quantity in ${file.parentFile.name}",
|
||||
resource.quantities.all { it in validQuantities },
|
||||
)
|
||||
assertTrue("$name in ${file.parentFile.name} is missing the required 'other' quantity", "other" in resource.quantities)
|
||||
}
|
||||
val baseArgs = placeholderIndices(base.text)
|
||||
val localeArgs = placeholderIndices(resource.text)
|
||||
assertTrue(
|
||||
"$name in ${file.parentFile.name} uses placeholders $localeArgs not present in the default $baseArgs",
|
||||
baseArgs.containsAll(localeArgs),
|
||||
assertEquals(
|
||||
"$name placeholders differ in ${file.parentFile.name}",
|
||||
placeholders(base.text),
|
||||
placeholders(resource.text),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -84,6 +90,6 @@ class StringResourcesTest {
|
||||
return (0 until items.length).joinToString(" ") { items.item(it).textContent }
|
||||
}
|
||||
|
||||
private fun placeholderIndices(text: String): Set<Int> =
|
||||
Regex("""%(\d+)\$""").findAll(text).mapTo(mutableSetOf()) { it.groupValues[1].toInt() }
|
||||
private fun placeholders(text: String): Map<Int, Char> =
|
||||
Regex("""%(\d+)\$([a-zA-Z])""").findAll(text).associate { it.groupValues[1].toInt() to it.groupValues[2][0] }
|
||||
}
|
||||
|
||||
@@ -17,6 +17,8 @@ class DownloadTreeTest {
|
||||
song("s3", "Three", "ar1", "Artist", "al2", "Second", size = 5),
|
||||
song("s4", "Four", "ar2", "Other", "al3", "Third", size = 1),
|
||||
),
|
||||
"Unknown artist",
|
||||
"Unknown album",
|
||||
)
|
||||
|
||||
assertEquals(listOf("Artist", "Other"), tree.artists.map { it.name })
|
||||
@@ -37,7 +39,7 @@ class DownloadTreeTest {
|
||||
|
||||
@Test
|
||||
fun aSongWithNoArtistOrAlbumIsStillShown() {
|
||||
val tree = buildTree(listOf(song("s1", "One", null, null, null, null, size = 3)))
|
||||
val tree = buildTree(listOf(song("s1", "One", null, null, null, null, size = 3)), "Unknown artist", "Unknown album")
|
||||
|
||||
val artist = tree.artists.single()
|
||||
assertEquals("Unknown artist", artist.name)
|
||||
|
||||
@@ -104,6 +104,20 @@ class AddSourceViewModelTest {
|
||||
assertEquals("Failed: Wrong username", state.message)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun aMalformedResponseIsReportedWithTheLocalizedMessage() {
|
||||
val viewModel = viewModel()
|
||||
viewModel.setAddress(server.url("/").toString())
|
||||
viewModel.setUsername("u")
|
||||
viewModel.setPassword("p")
|
||||
server.enqueue(MockResponse().setBody("not an xml document"))
|
||||
|
||||
viewModel.testConnection()
|
||||
await { !viewModel.state.value.busy && viewModel.state.value.isError }
|
||||
|
||||
assertEquals("Malformed response from the server", viewModel.state.value.message)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun aSuccessfulPingClearsAnEarlierError() {
|
||||
val viewModel = viewModel()
|
||||
|
||||
Reference in New Issue
Block a user