/* * Copyright (c) Double Symmetry GmbH * Commercial use requires a license. See https://rntp.dev/pricing */ package com.doublesymmetry.trackplayer.models import androidx.media3.common.MediaItem import androidx.media3.common.MediaMetadata import androidx.media3.exoplayer.ExoPlayer import org.junit.After import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertNull import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner import org.robolectric.RuntimeEnvironment import org.robolectric.annotation.Config @RunWith(RobolectricTestRunner::class) @Config(sdk = [33]) class MetadataApplierTest { private lateinit var player: ExoPlayer @Before fun setUp() { val context = RuntimeEnvironment.getApplication() player = ExoPlayer.Builder(context).build() } @After fun tearDown() { player.release() } private fun buildItem( id: String, title: String? = null, artist: String? = null, albumTitle: String? = null, genre: String? = null, ): MediaItem = MediaItem.Builder() .setMediaId(id) .setUri("https://example.com/$id.mp3") .setMediaMetadata( MediaMetadata.Builder().apply { title?.let { setTitle(it) } artist?.let { setArtist(it) } albumTitle?.let { setAlbumTitle(it) } genre?.let { setGenre(it) } }.build() ) .build() // ---- Happy path: live ICY-style updates rewrite the current item ---- @Test fun `applies title and artist to current item`() { player.setMediaItems(listOf(buildItem("radio"))) val event = MetadataReceivedEvent(title = "Hoppípolla", artist = "Sigur Rós") val applied = MetadataApplier.applyToCurrentMediaItem(player, event) assertTrue(applied) val updated = player.getMediaItemAt(0).mediaMetadata assertEquals("Hoppípolla", updated.title?.toString()) assertEquals("Sigur Rós", updated.artist?.toString()) } @Test fun `preserves existing user-supplied fields the event does not touch`() { player.setMediaItems( listOf( buildItem( id = "radio", title = "Static Station Title", artist = null, albumTitle = "My Radios", genre = "Rock", ) ) ) val event = MetadataReceivedEvent(title = "New Song", artist = "New Artist") MetadataApplier.applyToCurrentMediaItem(player, event) val updated = player.getMediaItemAt(0).mediaMetadata assertEquals("New Song", updated.title?.toString()) assertEquals("New Artist", updated.artist?.toString()) assertEquals("My Radios", updated.albumTitle?.toString()) assertEquals("Rock", updated.genre?.toString()) } @Test fun `overwrites previous stream-supplied title on next update`() { player.setMediaItems(listOf(buildItem("radio"))) MetadataApplier.applyToCurrentMediaItem( player, MetadataReceivedEvent(title = "Song A", artist = "Artist A"), ) MetadataApplier.applyToCurrentMediaItem( player, MetadataReceivedEvent(title = "Song B", artist = "Artist B"), ) val updated = player.getMediaItemAt(0).mediaMetadata assertEquals("Song B", updated.title?.toString()) assertEquals("Artist B", updated.artist?.toString()) } @Test fun `null fields in event do not clobber existing values (sticky semantics)`() { player.setMediaItems(listOf(buildItem(id = "radio", artist = "Existing Artist"))) val event = MetadataReceivedEvent(title = "Just a Title", artist = null) MetadataApplier.applyToCurrentMediaItem(player, event) val updated = player.getMediaItemAt(0).mediaMetadata assertEquals("Just a Title", updated.title?.toString()) assertEquals("Existing Artist", updated.artist?.toString()) } // ---- URI preserved → playback uninterrupted ---- @Test fun `preserves URI on rewrite`() { player.setMediaItems(listOf(buildItem("radio"))) val originalUri = player.getMediaItemAt(0).localConfiguration?.uri.toString() MetadataApplier.applyToCurrentMediaItem( player, MetadataReceivedEvent(title = "Song", artist = "Artist"), ) val newUri = player.getMediaItemAt(0).localConfiguration?.uri.toString() assertEquals(originalUri, newUri) } @Test fun `preserves mediaId on rewrite`() { player.setMediaItems(listOf(buildItem("my-radio-id", title = "Original"))) MetadataApplier.applyToCurrentMediaItem( player, MetadataReceivedEvent(title = "Live Title", artist = null), ) assertEquals("my-radio-id", player.getMediaItemAt(0).mediaId) } // ---- artworkUri / albumTitle / genre coverage ---- @Test fun `applies artworkUri and genre from event`() { player.setMediaItems(listOf(buildItem("radio"))) val event = MetadataReceivedEvent( title = "Song", artist = "Artist", albumTitle = "Album", artworkUri = "https://art.example.com/cover.jpg", genre = "Indie", ) MetadataApplier.applyToCurrentMediaItem(player, event) val updated = player.getMediaItemAt(0).mediaMetadata assertEquals("Album", updated.albumTitle?.toString()) assertEquals("https://art.example.com/cover.jpg", updated.artworkUri?.toString()) assertEquals("Indie", updated.genre?.toString()) } // ---- No current item → no-op ---- @Test fun `returns false when queue is empty`() { val event = MetadataReceivedEvent(title = "Won't Apply", artist = null) val applied = MetadataApplier.applyToCurrentMediaItem(player, event) assertFalse(applied) assertEquals(0, player.mediaItemCount) } // ---- Affects only the current index, not siblings ---- @Test fun `only mutates the current item, not the rest of the queue`() { player.setMediaItems( listOf( buildItem("track-1", title = "Title 1"), buildItem("track-2", title = "Title 2"), buildItem("track-3", title = "Title 3"), ) ) val event = MetadataReceivedEvent(title = "Live Title 1", artist = null) MetadataApplier.applyToCurrentMediaItem(player, event) assertEquals("Live Title 1", player.getMediaItemAt(0).mediaMetadata.title?.toString()) assertEquals("Title 2", player.getMediaItemAt(1).mediaMetadata.title?.toString()) assertEquals("Title 3", player.getMediaItemAt(2).mediaMetadata.title?.toString()) } // ---- Edge case: event with all null fields contributes nothing ---- @Test fun `event with all null fields leaves item unchanged but still reports applied`() { player.setMediaItems(listOf(buildItem(id = "radio", title = "Original Title"))) val event = MetadataReceivedEvent( title = null, artist = null, albumTitle = null, artworkUri = null, genre = null, ) val applied = MetadataApplier.applyToCurrentMediaItem(player, event) assertTrue(applied) assertEquals("Original Title", player.getMediaItemAt(0).mediaMetadata.title?.toString()) assertNull(player.getMediaItemAt(0).mediaMetadata.artist) } }