metadata: refresh the artist screenshot with the new biography

Regenerate the store screenshots from the demo library. Only the artist
page changed meaningfully, so keep that capture, using Nine Inch Nails,
which has a biography and three albums.

The demo test now fetches the artist biography and passes artist art to
the home, library and search screens, so a regeneration keeps their art
instead of falling back to letter placeholders.

The captures now come from a 393x786 dp, 440 dpi phone: Pixel XL's
640x360 dp renders text far larger than a real phone shows it. That
phone renders 1080x2161, one pixel over the 2:1 limit both stores
enforce, so captures are written out at 1080x2160.
This commit is contained in:
austinried
2026-10-24 02:16:45 +00:00
parent 05e318f9ab
commit d7a3a41ab2
8 changed files with 26 additions and 3 deletions
@@ -40,12 +40,14 @@ import com.subtracks.data.model.Source
import com.subtracks.data.model.coverArtKey import com.subtracks.data.model.coverArtKey
import com.subtracks.data.prefs.ListQuery import com.subtracks.data.prefs.ListQuery
import com.subtracks.data.repo.SEARCH_RESULT_LIMIT import com.subtracks.data.repo.SEARCH_RESULT_LIMIT
import com.subtracks.data.repo.normalizeBiography
import com.subtracks.data.source.subsonic.SubsonicSource import com.subtracks.data.source.subsonic.SubsonicSource
import com.subtracks.data.source.subsonic.TestServer import com.subtracks.data.source.subsonic.TestServer
import com.subtracks.data.source.subsonic.TestServers import com.subtracks.data.source.subsonic.TestServers
import com.subtracks.data.sync.SyncService import com.subtracks.data.sync.SyncService
import com.subtracks.playback.PlaybackState import com.subtracks.playback.PlaybackState
import com.subtracks.playback.QueueItem import com.subtracks.playback.QueueItem
import com.subtracks.ui.components.ArtistArt
import com.subtracks.ui.home.HomeFeed import com.subtracks.ui.home.HomeFeed
import com.subtracks.ui.home.HomeScreen import com.subtracks.ui.home.HomeScreen
import com.subtracks.ui.library.AlbumDetailScreen import com.subtracks.ui.library.AlbumDetailScreen
@@ -82,11 +84,15 @@ import javax.imageio.ImageIO
* by `:*UnitTest` or the integration suite; run it explicitly with `gradle :app:demoScreenshots`. * by `:*UnitTest` or the integration suite; run it explicitly with `gradle :app:demoScreenshots`.
* The synced database is rebuilt each run; downloaded cover art is cached under the system temp dir * The synced database is rebuilt each run; downloaded cover art is cached under the system temp dir
* so reruns do not re-fetch it. * so reruns do not re-fetch it.
*
* The device is a 393x786 dp phone at 440 dpi, which renders 1080x2161. A capture one pixel over
* 1080x2160 breaks the stores' 2:1 limit, so the captures are cropped to 1080x2160 afterwards, with
* the last row taken from the row above it.
*/ */
@OptIn(DelicateCoilApi::class) @OptIn(DelicateCoilApi::class)
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@GraphicsMode(GraphicsMode.Mode.NATIVE) @GraphicsMode(GraphicsMode.Mode.NATIVE)
@Config(qualifiers = RobolectricDeviceQualifiers.PixelXL) @Config(qualifiers = "w393dp-h786dp-normal-long-notround-any-440dpi-keyshidden-nonav")
class DemoScreenshotTest { class DemoScreenshotTest {
@get:Rule @get:Rule
val composeRule = createAndroidComposeRule<ComponentActivity>() val composeRule = createAndroidComposeRule<ComponentActivity>()
@@ -123,6 +129,7 @@ class DemoScreenshotTest {
HomeScreen( HomeScreen(
feed = demo.feed, feed = demo.feed,
coverArt = demo.coverArt, coverArt = demo.coverArt,
artistArt = demo::artistArt,
playingSongId = demo.homePlayingSong.id, playingSongId = demo.homePlayingSong.id,
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
) )
@@ -178,6 +185,7 @@ class DemoScreenshotTest {
ArtistDetailScreen( ArtistDetailScreen(
artist = demo.artist, artist = demo.artist,
artistName = demo.artist.name, artistName = demo.artist.name,
bio = demo.artistBio,
albums = albums =
ArtistAlbums( ArtistAlbums(
albumCredits = albumCredits =
@@ -216,6 +224,7 @@ class DemoScreenshotTest {
onSync = {}, onSync = {},
onOpenSettings = {}, onOpenSettings = {},
artwork = demo.artwork(demo.nowPlayingAlbum.coverArt), artwork = demo.artwork(demo.nowPlayingAlbum.coverArt),
artistArt = demo::artistArt,
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
) )
demo.MiniPlayerOverlay(demo.nowPlayingSong, demo.nowPlayingAlbum, progressInset = true) demo.MiniPlayerOverlay(demo.nowPlayingSong, demo.nowPlayingAlbum, progressInset = true)
@@ -228,6 +237,7 @@ class DemoScreenshotTest {
onQueryChange = {}, onQueryChange = {},
results = demo.search, results = demo.search,
coverArt = demo.coverArt, coverArt = demo.coverArt,
artistArt = demo::artistArt,
playingSongId = demo.nowPlayingSong.id, playingSongId = demo.nowPlayingSong.id,
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
) )
@@ -288,7 +298,7 @@ class DemoScreenshotTest {
private fun reencodeOpaqueRgb(file: File) { private fun reencodeOpaqueRgb(file: File) {
val decoded = ImageIO.read(file) val decoded = ImageIO.read(file)
val rgb = BufferedImage(decoded.width, decoded.height, BufferedImage.TYPE_INT_RGB) val rgb = BufferedImage(decoded.width, decoded.width * 2, BufferedImage.TYPE_INT_RGB)
rgb.createGraphics().run { rgb.createGraphics().run {
drawImage(decoded, 0, 0, null) drawImage(decoded, 0, 0, null)
dispose() dispose()
@@ -329,6 +339,7 @@ class DemoScreenshotTest {
val homePlayingAlbum: Album, val homePlayingAlbum: Album,
val homePlayingSong: Song, val homePlayingSong: Song,
val artist: Artist, val artist: Artist,
val artistBio: String?,
val artistAlbums: List<Album>, val artistAlbums: List<Album>,
val search: SearchResults, val search: SearchResults,
val coverArt: (String?, Boolean) -> CoverArtRef?, val coverArt: (String?, Boolean) -> CoverArtRef?,
@@ -371,6 +382,15 @@ class DemoScreenshotTest {
return artworkColorsFromSeeds(primary?.rgb ?: FALLBACK_SEED, null) return artworkColorsFromSeeds(primary?.rgb ?: FALLBACK_SEED, null)
} }
fun artistArt(artist: Artist): ArtistArt {
val thumbnail = artist.coverArt?.let { coverArt(it, true) }
return ArtistArt(
image = thumbnail,
thumbnail = thumbnail,
covers = artist.coverArts.take(4).mapNotNull { coverArt(it, true) },
)
}
companion object { companion object {
private const val FALLBACK_SEED = 0xFF3A7BD5.toInt() private const val FALLBACK_SEED = 0xFF3A7BD5.toInt()
private const val DEMO_BASE = "https://demo.navidrome.org/" private const val DEMO_BASE = "https://demo.navidrome.org/"
@@ -417,9 +437,11 @@ class DemoScreenshotTest {
val homePlayingAlbum = allAlbums.firstOrNull { it.name == "Shaking The Habitual" } ?: allAlbums.last() val homePlayingAlbum = allAlbums.firstOrNull { it.name == "Shaking The Habitual" } ?: allAlbums.last()
val homePlayingSong = songsOf(homePlayingAlbum).first() val homePlayingSong = songsOf(homePlayingAlbum).first()
val artist = val artist =
allArtists.firstOrNull { it.name == "Ugress" } allArtists.firstOrNull { it.name == "Nine Inch Nails" }
?: allArtists.firstOrNull { it.name == "pornophonique" }
?: allArtists.maxByOrNull { it.albumCount } ?: allArtists.maxByOrNull { it.albumCount }
?: error("demo library has no artists") ?: error("demo library has no artists")
val artistBio = normalizeBiography(source.artistBiography(artist.id))
val artistAlbums = dao.albumsForArtist(1, artist.id).first() val artistAlbums = dao.albumsForArtist(1, artist.id).first()
val coverArtIds = val coverArtIds =
@@ -509,6 +531,7 @@ class DemoScreenshotTest {
homePlayingAlbum = homePlayingAlbum, homePlayingAlbum = homePlayingAlbum,
homePlayingSong = homePlayingSong, homePlayingSong = homePlayingSong,
artist = artist, artist = artist,
artistBio = artistBio,
artistAlbums = artistAlbums.filter { it.coverArt != null }, artistAlbums = artistAlbums.filter { it.coverArt != null },
search = search, search = search,
coverArt = coverArt, coverArt = coverArt,
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 513 KiB

After

Width:  |  Height:  |  Size: 577 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 MiB

After

Width:  |  Height:  |  Size: 3.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 MiB

After

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 469 KiB

After

Width:  |  Height:  |  Size: 421 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 410 KiB

After

Width:  |  Height:  |  Size: 371 KiB